Scope Architect (pure worker v1.0)
Slice by flow, never by directory — and write it as a contract a hook can enforce.
Groups a feature's tasks into independent, vertically-sliced scopes and writes each as a
committed contract (shapeup/<slug>/scopes/<scope-id>.md) the rest of the
harness enforces mechanically: the sandbox hook denies writes outside a substrate, t0-verify
runs the fixtures, the evaluator asserts only against the affordance manifest. This skill is
the sole writer of scope contracts — a distinct authority from the planner (task
decomposition) and a distinct failure mode (directory-thinking, PA1) deserving its own
the ship report's census table.
Input contract — the WorkOrder
| Field | What it is |
|---|---|
operation |
map-scopes — the only operation this skill has. It covers first slicing after the board exists, folding discovered items in, and re-slicing a stuck scope; the payload says which of those you are doing |
payload.feature / payload.spec_folder |
Slug + committed spec (read ux-behavior.md for manifests; usecases for flows) |
payload.tasks[] |
The board's tasks with their touched files — the slicing INPUT only. Each carries use_case_refs; those UC ids are what you write into the contract. Never copy a task id into a contract |
substrate.allowed |
scopes/*.md + scope-board.md — your ONLY write surface |
Core process
1 SLICE build an import/business-flow graph over the tasks' touched files (grep heuristic
is fine; AST is an optimization). One scope = one call chain: the UI screen + the
API route + the use case + the repository it drives. Scopes aligning 1:1 with a
top-level directory (all-frontend, all-backend) FAIL — that is layer-thinking.
2 CLASSIFY topology_type: LAYER_CAKE (thin balanced UI+backend) | ICEBERG (complexity on one
side) | CHOWDER (true strays with no shared flow — the one deliberate exception)
3 CONTRACT per scope, write scopes/<scope-id>.md — MARKDOWN (ADR-0001): frontmatter for
scalars and [a, b] lists, a `## Affordances` table for affordance_manifest, and a
short `## Why this slice` paragraph. A reviewer must be able to read the substrate
in a PR; regeneration preserves prose under headings you do not own.
scope_id, topology_type — the stable join key is the scope
use_cases[] — the UC ids this scope implements.
THE ONLY LINK YOU WRITE TO THE
WORK: never task ids. The contract
is committed and the board is not,
so a TASK-NNN here dangles on
every other clone (spec-lint
TIER-DIRECTION reds it). The
scope's tasks are re-derived from
the board's own use_case_refs
covers[] — optional REQ-ids from
requirements.md this scope answers
for; stable, never renumbered
depends_on[] — scope_ids this scope builds AFTER.
This is the build ORDER — declare
it whenever one scope consumes
another's output, or the two race
allowed_file_substrate[] — exact globs; the sandbox hook's
write-whitelist; wrong here =
a legitimate ESCALATE later
shared_substrate[] — files ≥2 scopes both touch;
every write there forces a full
seesaw run at the next gate
affordance_manifest — from ux-behavior.md state
tables: every interactive
element as {test_id, role} +
required_states [idle, loading,
success, error, empty]
e2e_verification_fixtures[] — the command(s)/spec file(s)
that drive this scope
end-to-end (T0 layer); too
speculative to fixture → mark
TBD and flag it, never invent
a fixture for unbuilt behavior
hill_phase: "UPHILL_UNKNOWN" — ALWAYS; phase is derived from
T0/T1/seesaw facts later,
never authored
4 LINT node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify spec --slug <slug>
→ PA1 (directory alignment), PA2 (>~15 files), DISJOINT (undeclared overlap),
SCOPE-ANCHOR (empty/unresolvable use_cases), TIER-DIRECTION (a task id in a
committed contract), SCOPE-DEPS (depends_on naming a scope that isn't here).
Fix reds by re-slicing, not by silencing.
5 BOARD regenerate scope-board.md — a VIEW of the contracts, nothing more:
| scope_id | topology | use_cases | depends_on | files | lint |
Every column restates a field the contract already declares, so the board can be thrown
away and rebuilt. Do NOT add a `wave` column: waves are Kahn levels of `depends_on` and
`probe resume` derives them at dispatch — a hand-written copy of a derived value drifts,
which is exactly why `unlocks` stopped being authored. The BUILD ORDER lives in each
contract's `depends_on`; the board only shows it.
A `TASK-` id anywhere in a contract or the board — a column, a cell, or a sentence in
the prose — is spec-lint TIER-DIRECTION red. The board is committed; ids are not.
Folding in a discovered item: it joins the nearest scope only if the flow matches (extend that
substrate minimally); otherwise propose a NEW scope — never silently widen an existing one.
Re-slicing a stuck scope: re-run step 1 on just that scope's task+file set → N new contracts;
mark the old one superseded_by: [ids] — never delete (branch and T0 history stay attributable).
Anti-rationalization table
| Excuse | Reality |
|---|---|
| "The discovered item obviously fits scope A" | Run the flow match. 'Obviously' is how substrates silently widen. |
| "One scope per directory is cleaner" | That's PA1 — a layer, not a flow. A scope must ship something a user can do. |
| "I'll widen the substrate a little so the doer stops escalating" | A wide substrate is no substrate. Split or add a shared_substrate entry, deliberately. |
| "This scope looks downhill, I'll set the phase" | hill_phase is UPHILL_UNKNOWN at write, always. Facts move dots, not authors. |
| "The old contract is superseded, delete it" | supersede-never-delete. History must stay attributable. |
| "Both scopes implement that UC, the tasks will sort themselves out" | They will not — both scopes get every task of that UC and three of four writes get denied. Give each scope its own use cases, or say so in deviations[] so the board can be stamped. |
| "I'll list the task ids so the contract says what it builds" | The board is gitignored and renumbers per machine; the contract is committed. Cite the UCs — the tasks are re-derived from them. |
| "Build order is obvious from the slice, I'll leave depends_on empty" | Nothing infers it any more. An undeclared edge means the two scopes are released into the same wave and race. |
| "Fixtures can come later, leave the field empty" | Fixture at contract time or an explicit TBD flag — silence is how T0 goes blind. |
Output contract — the WorkResult
Escalation rule. If you return status: "escalated", the first entry in deviations[]
must be the blocker: one specific, answerable question plus the context needed to answer it.
Nothing else in the envelope carries it — there is no escalates[] field — so a vague entry, or
the question buried under other notes, reaches the human as "something went wrong" and costs a
round. Write it so someone without your context can answer it in one reply.
scopes/*.md + scope-board.md in your substrate, then
.shapeup/<slug>/results/<order-suffix>.json: status, artifacts[] (the contracts
written/superseded), deviations[] (e.g. a discovered item implying a new UC — the planner's
territory — and any lint warn left standing, with why). You never touch task files,
tasks/_index.md, spec docs, or run-state.
Verification checklist
- Every scope crosses layers or is declared CHOWDER; spec-lint PA1 = 0 red
- Every scope names ≥1
use_casesthat resolves on disk, and NO contract carries a task id - Every scope that consumes another's output declares it in
depends_on - Substrates disjoint except declared shared_substrate (DISJOINT = 0 red)
- Every interactive element in scope screens appears in exactly one affordance_manifest
- Every scope has fixtures or an explicit TBD flag
- Every hill_phase written is UPHILL_UNKNOWN; superseded contracts kept
- The WorkResult validates against
work-result.schema.json
Invocation
# Orchestrated — compile-order --operation map-scopes --worker scope-architect …
/scope-architect --order .shapeup/checkout-vnpay/orders/map-scopes.json
# Standalone shims (compile the same envelope)
/scope-architect --map shapeup/checkout-vnpay/
/scope-architect --map --split cart-creation shapeup/checkout-vnpay/ # re-slice one scope