# Decompose Plan

> Cut a unit of work into children the plan gate will accept — testable acceptance criteria in EARS, disjoint or declared scope globs, an acyclic dependency graph, a token budget, an integrity level, and the command that demonstrates each child end to end. Use at DECOMPOSE, when a plan gate has just refused a child, or when a child turns out to have no consumer-visible behaviour to check.

- Skill: `niksavis/decompose-plan` (Agent Skill)
- Install (CLI): `npx skillmds@latest add niksavis/decompose-plan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/niksavis/decompose-plan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Finance & Business
- Author: niksavis (https://skillmd.com/u/niksavis)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/niksavis/decompose-plan

---

<!-- Generated by `basicly skills-build` from skill.yaml. Do not edit; edit the source. -->

# 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.

