Decompose a plan the gate will accept
The plan gate sits on entry to BUILD, not exit. Everything below is something it can refuse, and everything it refuses is cheap here and expensive one state later — BUILD is where nearly all tokens go.
The five fields, and what each one is really for
| Field | Refused when | Why it is on the gate |
|---|---|---|
| Acceptance criteria | absent | The gate checks presence only (plan_gate.missing_fields tests bool(acceptance)), so the EARS discipline is yours to keep: it distinguishes trigger, state, condition, feature-gated and ubiquitous, GWT collapses all five, and that distinction is what makes a check derivable |
| Scope globs | absent | Overlap decides what serializes. Two children writing one path collide at the merge queue, at a rework retry each |
| Dependencies | cyclic | A cycle is a plan that cannot start |
| Token budget | absent | The band prices the lane before it runs |
| Integrity level | absent | It selects the gates and the rework allowance |
The sixth field, which is the one that catches real defects
Every child names how it is demonstrated end to end — a command to run, a request to make, or a test that exercises it through the consumer surface. The field is refused on two grounds: absent, and present but naming nothing runnable, where runnable means a backticked span.
A child that cannot name one has been sliced horizontally, and that is the failure this field exists to catch. It is also why criterion-derived checks cannot be satisfied afterwards: a horizontal child has no consumer-visible behaviour, so there is nothing to derive a check from.
Slice vertically, and know why you will not want to
The tidy cut is horizontal — all the schema in one child, all the tests in another, all the CLI wiring in a third. It is tidy because scope-glob overlap is file adjacency, so a horizontal cut produces the cleanest-looking scopes. It is also the cut where no child does anything a consumer can see.
Cut along change-shape instead. It is derived from the call graph rather
than authored, which is exactly why it can express a slice that crosses
directories.
Locate the producer, never the surface
A scope is a claim about where a wrong value is produced, and the tempting answer is where it is displayed. They are usually different modules, and the gap is only found when a lane has already spent a budget reaching it.
Four scopes were written wrong in one session on 2026-08-22, all the same way:
| declared | the value is actually produced in |
|---|---|
loop.py, supervise.py, verify.py |
checkout.py, commit.py, merge.py |
board_wall.py |
board_regions.py |
| two skill surfaces | the YAML source plus both projected roots — .claude/skills and .agents/skills |
board_render.py |
board_regions.py |
One was bounced at the landing gate for changing eleven files outside its
declared four — about thirty million tokens of rework, on work that was
already correct. One was caught by projection-skills refusing the commit.
Two were caught by probing before dispatch. Only the probe is cheap.
The probe is one command. Take a literal string the wrong output prints, or the name of the function that returns the wrong value, and grep for it. The file that emits it is the scope; the file that renders it usually is not:
rg -n '<the literal the defect prints>' src/
rg -n 'def <the function whose result is wrong>' src/
Then ask the question that catches the rest: does this value have more than
one consumer, and does it reach more than one surface? A projected artifact
usually has several — basicly skills-build writes two roots, and a scope
naming one of them fails the projection gate. A renderer
almost always sits one call above a builder that owns the fact.
A scope that names a file the fix never touches is not a smaller claim than the truth. It is a different claim, and the merge queue is where it is corrected.
Declare the scope honestly even though it costs you
A scope declared honestly reads as a bigger lane, because the band prices what it reads. Declare it anyway. One bead moved from 78,709 to 197,646 to 245,466 tokens on an unchanged diff as its scope was corrected, and the version that under-declared was the version that collided at landing.
Name the child's own file, never a directory glob
The band prices every file a scope glob matches, not the files the change will
touch. On 2026-08-27 a plan declared changelog.d/*.md and basicly.d/*.toml on
each of five children; the estimator read all 147 fragments into every lane and
forecast 50M tokens each. The same day seu7rx was refused at 305,831 tokens
because its scope said .claude/skills/** for a change to five skills. Name the
file the child will create: changelog.d/<epic-id>.<n>.<category>.md and
basicly.d/<epic-id>.<n>.toml, where <n> is the child's position in the plan
(children are minted <epic-id>.1, .2, ...). For projected output name the
projected file, .claude/skills/<name>/SKILL.md, not the tree. A scope whose glob
matches more than a handful of files is a population, and the dry run should make
you say why (basicly-tskiym adds the warning).
Flatten, never deepen
A child that needs children of its own is a child you cut wrong. Nesting moves the same work behind another gate rather than making any piece smaller, and the WIP bound counts leaves.
Report size, do not refuse it
A child whose forecast implies a diff far past reviewable is reported, not refused. A large diff is sometimes correct — a mechanical rename is one — and the remedy is the author's call while splitting is still cheap.