/substrate:graph-spec
Turn a finished spec into a bead DAG: one epic + child beads wired by blocked-by: edges, all tagged with the canonical epic label epic:<slug>. The DAG is the input to substrate's parallel-execution doctrine — beads in the same topological wave touch no shared blocker and can be dispatched to worktree-isolated subagents at once.
This skill produces the graph; it does not run it. Execution is the parallel-execution orchestrator's job (docs/doctrine/agents-parallel-execution-doctrine.md), or a plain phase-by-phase /substrate:execute pass.
Arguments
<spec-path> — path to a spec at docs/tasks/ongoing/<slug>/<slug>-spec.md. If omitted, discover it (see Step 1).
When to run
- A spec exists and its Prompt Execution Strategy section is filled (phases → steps → verify → gate).
- You want the work decomposed into independently-dispatchable beads before execution — especially when phases contain file-disjoint steps that could run in parallel.
- Invoked automatically as the last step of
/substrate:architect-spec, or by hand on any existing spec.
When to REFUSE
Fail fast — abort with a one-line explanation, never fall back to a guess.
| Signal |
Action |
No spec path given AND none discoverable under docs/tasks/ongoing/**/*-spec.md |
Ask the user for the spec path. |
Spec has no "Prompt Execution Strategy" section (per execution-format.md) |
Abort: "This spec has no execution strategy to decompose. Run /substrate:architect-spec to produce one." |
| Bead-tracker unresolvable (see Step 2) |
Abort: state whether tbd or a markdown fallback is expected, and why neither resolved. |
An epic already exists for this slug (tbd list --type epic --label epic:<slug> non-empty) |
Do NOT double-create. Skip to Step 6 and render the existing DAG; offer to add only the missing beads. |
Protocol you operate under
- Execution grammar:
docs/protocol/sdd/execution-format.md (phases → steps → verify → gate) — the structure you decompose.
- DAG algorithm: mirrors
/substrate:synthesize-session Step 8 (pairwise blocked-by inference + Kahn cycle detection). Same "bead DAG" dialect on purpose — one graph vocabulary across the plugin.
- Consumer:
docs/doctrine/agents-parallel-execution-doctrine.md (single-writer tracker, integration branch, file-disjoint waves).
Workflow
Step 1 — Resolve the spec
If a path was passed, use it. Else glob docs/tasks/ongoing/**/*-spec.md:
- exactly one match → use it;
- several → list them and ask which;
- none → REFUSE per the table.
Derive <slug> from the containing directory name (docs/tasks/ongoing/<slug>/...). That slug is the whole coordination key: the epic label is epic:<slug>, deterministically, so /substrate:synthesize-session later files follow-up beads under the same label without any handshake.
Read the spec. Confirm it has a Prompt Execution Strategy section; if not, REFUSE.
Step 2 — Resolve the bead-tracker
Same resolution as /substrate:synthesize-session (keep them identical):
- If
.substrate/config.json sets "bead-tracker", honor it (tbd | none | other).
- Else auto-detect
tbd: .tbd/config.yml exists AND a tbd binary is callable — command -v tbd succeeds, OR npx --no-install get-tbd --version succeeds. Else none.
tbd → beads are canonical; this skill creates them via tbd create. Substitute npx --no-install get-tbd for tbd throughout if no global binary is on PATH.
none → markdown is canonical; beads live at docs/tasks/ongoing/<slug>/beads/<bead-slug>.md. blocked-by: in frontmatter is the only edge record (no tbd dep).
Step 3 — Decompose the spec into bead candidates
Walk the Prompt Execution Strategy. Each step (#### Step N.M) is one bead candidate; collapse trivially-coupled sibling steps only when they edit the same file and share a single Verify block. For each candidate capture, in memory (no writes yet):
- title — imperative, scoped (e.g. "Add
requireStoreOwner guard to convex/stores.ts").
- phase / layer — the owning phase and its
layer-hint (domain / backend / frontend / infra / cross-cutting).
- creates — files, symbols, exports, tables the step introduces or modifies (read the step body + its Verify block).
- consumes — files/symbols/commands the step depends on existing.
- invalidated tests (reconcile) — existing tests that assert behavior this bead changes (distinct from any new tests the step adds). A behavior change to an existing symbol / contract / exported member / required-dependency almost always breaks a test that pinned the old behavior, yet the step body rarely names that test. Reverse-scan the test tree for tests referencing the changed symbol/file/contract and fold each matching test file into this bead's creates (its write-scope) with a one-line
reconcile: note ("asserts prior behavior of X"). Skipping this is the single most common cause of a mid-run stall: the test lives outside the bead's declared Files, so the group-runner can't touch it and the break surfaces only at the integrated re-gate. Purely-additive beads (new files, no change to an existing symbol) have no invalidated tests — leave this empty.
- gate — the step's Verify commands, inlined (a subagent runs these; per the parallel-execution doctrine it never touches tbd or git). Compare it to
substrate.yaml's gate.{compile,test,lint}: if the bead's gate is a strict subset of gate.*, or omits a suite the bead's layer is actually covered by (e.g. a frontend bead that runs only tsc, not the vitest suite), tag the bead gate-scope: partial in its body. That tells the orchestrator its per-bead green is a fast pre-check only, and that the wave's union re-gate — not this narrow gate — authorizes the merge (agents-parallel-execution-doctrine.md §Supporting → Re-run the gate on the integrated branch). A gate that equals or supersets gate.* needs no tag.
- acceptance criterion — binary pass/fail lifted from the step + its gate.
Step 4 — Build the DAG
Mirror /substrate:synthesize-session Step 8:
- Pairwise scan. For each ordered pair
(A, B), mark B blocked-by A iff B's consumes references a file/symbol/export/table in A's creates. This is richer than the linear phase order: two steps in the same phase that touch disjoint files land with no edge between them → same wave → parallel. Record a one-line reason per edge.
- Layer backstop. If two beads share a file in their creates set, serialize them with an edge (later phase blocked-by earlier) even absent a symbol dependency — the parallel-execution doctrine forbids two beads editing one file in a wave.
- Cycle detection (Kahn). Compute in-degrees; peel zero-in-degree nodes. If any remain → a cycle exists. REFUSE: print the cycle (
A → B → C → A) and ask the user to split or drop a bead. Never emit a cyclic DAG.
- Encode
blocked-by: on each bead.
Step 4.5 — Partition into context-budget windows
The DAG is now cycle-free; cut it into agent-sized windows so no group-runner's context
rots or auto-compacts. Read execution.context-budget from substrate.yaml (default 0.4 if
absent — a deviatable prior, not a hard gate). Then:
- Estimate per-bead cost. Walk beads in topological order. For each, estimate
cost = Σ(bytes of its Files/creates+consumes) + heavy-ref surcharge (schema / contract / migration reads the bead must load) + gate-log weight + effort(XS…L). This is a heuristic
prior, not a measurement — round generously.
- Accumulate into windows. Open
window-1; add beads in topological order, summing cost.
When the running total would cross context-budget (as a fraction of one agent's usable
window), close the current window and open the next. Snap boundaries to file-adjacency:
never split a chain of co-edited beads (overlapping Files) across two windows — co-edited
beads share a warm worktree, so they belong to the same window; file-disjoint chains fall
into separate windows (isolation, parallel where edges allow). Adjacency wins over the raw
cost cut when they conflict.
- Flag under-decomposition. If a single bead's cost alone exceeds
context-budget, do
not silently over-fill a window: warn and recommend splitting that bead (it is too
heavy for one runner), then place it in its own window so the run can still proceed.
The partition is a deviatable prior: the orchestrator MAY re-batch at dispatch time (logging
the deviation). See agents-parallel-execution-doctrine.md §Grouping & windows.
Step 4.55 — Bind doctrine to write-scope (doctrine:<id> labels)
Windows exist and every bead has a write-scope. Bind each bead to the doctrines that govern the
files it will edit, at graph time, so a dispatcher can push the right doctrine into the
worker's prompt instead of hoping the worker goes looking. Only the manifest's paths key answers
the file question — triggers matches a brief (prose) at spec time, paths matches a
write-scope (files) at graph time (docs/doctrine/agents-doctrine.md §3.1).
- Read the manifest at
docs/doctrine/doctrine-manifest.yaml. Take each entry's id and its
optional paths:, parsed in inline-list form only — paths: [docs/doctrine/**, x/y.sh], one
line — exactly how doctrine-lint.sh parses pointers. A block sequence (paths: then - a/**)
is not the declared form and the tooling cannot read it: report it as a manifest bug, do not
accommodate it.
- Silent no-op when there is nothing to bind. No manifest, or no entry declares
paths: →
stamp nothing and say nothing. Not a warning, not an abort: substrate must keep graphing specs in
repos that never adopted the key, and a warning would fire on every run in every one of them.
- Intersect. A bead's write-scope is creates ∪ modifies — Step 3's
creates set, which has
already absorbed the invalidated-test files. consumes is read-only and does not bind. A bead
is bound to <id> iff any path in its write-scope matches any glob in that entry's paths:.
Match relative to the repo root and permissively — a * may cross / (the same reading
doctrine-lint.sh uses for its rot guard), so docs/doctrine/** matches
docs/doctrine/agents-doctrine.md, and a bead whose declared scope is itself a directory glob
(convex/**) matches a doctrine glob rooted there.
- Stamp
doctrine:<id> — one label per matching entry; a bead may carry zero, one, or several.
<id> is the manifest's id field verbatim (not the filename, not the path). It is the key
downstream consumers resolve with bash docs/scripts/doctrine-digest.sh <id>; any other spelling
breaks that chain silently.
- Exempt the terminal node. The doctrine-reconciliation bead Step 4.6 shapes (the spec's
Phase N: Doctrine Reconciliation step, or the node you synthesize when the spec lacks one) gets
no generic doctrine:<id> labels — even though its docs/doctrine/** write-scope would match
every doctrine that governs doctrine files. Its whole brief is reconciliation against the fully
integrated epic; stamping it would bind it to a near-arbitrary subset and drown the signal. Step
4.6's contract is the only one it carries.
Worked example, against this repo's manifest (agents → paths: [docs/doctrine/**, docs/scripts/doctrine-lint.sh]; agents-parallel-execution → paths: [docs/scripts/bead-graph.sh]):
| bead |
write-scope |
labels stamped |
| Add rule 6 to the linter |
docs/scripts/doctrine-lint.sh |
doctrine:agents |
Teach bead-graph.sh a flow view |
docs/scripts/bead-graph.sh |
doctrine:agents-parallel-execution |
Document paths: in §3 |
docs/doctrine/agents-doctrine.md, docs/doctrine/doctrine-manifest.yaml |
doctrine:agents (via docs/doctrine/**) |
| Rename a Convex table |
convex/schema.ts |
(none — no entry's paths: covers it) |
Terminal kind: doctrine-reconciliation |
docs/doctrine/** |
none — exempt per (5), though the globs would match |
Step 4.6 — Force the terminal doctrine-reconciliation node
Every spec ends with Phase N: Doctrine Reconciliation (per spec-template.md). Its bead is not an ordinary node — it is the epic's mandatory terminal node that applies the ratify-only doctrine change the feature earned, in-epic. It never queues a doctrine-amendment and there is no downstream sink. Shape it explicitly, overriding the generic decomposition:
- Tag it
kind: doctrine-reconciliation (an additive tag alongside epic:<slug>), so the orchestrator can identify the terminal node.
- Edge it
blocked-by every other bead in the epic. It must see the fully integrated feature — no other bead may land after it. This is deliberately the one node that collapses parallelism at the end; that's correct.
- Write-scope =
docs/doctrine/**. Put docs/doctrine/** in its creates/Files. Editing doctrine files is ordinary working-tree change a group-runner may make in its worktree — it is not a tracker write, so the single-writer invariant is untouched.
- Solo terminal window. Give it its own
group:<window-N> as the last window (it is blocked-by everything, so it is structurally its own final wave regardless). Never fold it into a feature window.
- Ratify-only gate. Its gate is the epic's full union gate re-run on the integrated tip — green proves the mutation only codified what the code already did. Do not tag it
gate-scope: partial.
If the spec somehow lacks a Doctrine Reconciliation phase, synthesize this node anyway — it is mandatory per the contract; warn that the spec was missing it.
Step 5 — Persist the epic + beads
Preview the full bead list in DAG order inline, then ask: Create 1 epic + N beads under epic:<slug> now? (Y / n / select) — default Y (binary gate, no default-escape suffix). select enters a per-bead y / n / skip loop.
Branch A — tbd (single-writer; you are the only writer):
- Epic bead:
tbd create "Epic: <spec title>" --type epic -l "epic:<slug>" --file <spec-ref> (a tempfile holding the spec path + one-line summary; mktemp, unlink after). Capture its id as <epic-id>.
- Child beads, in DAG order so
blocked-by: resolves to already-assigned ids:
- render the bead body (acceptance criterion + inlined gate + state-transfer prompt) to a tempfile,
tbd create "<title>" --type task --parent <epic-id> -l "epic:<slug>" -l "group:<window-N>" [-l "doctrine:<id>" …] --file <tmp>, where <window-N> is the bead's window from Step 4.5 and the zero-or-more doctrine:<id> flags are its Step-4.55 bindings, one -l each (add -l "kind:doctrine-reconciliation" for the terminal node from Step 4.6 — which carries no doctrine: labels); capture the id, unlink the tempfile (unconditional cleanup, even on failure),
- stamp the spec back-link so a cold runner can re-open context: include
spec: <spec-path>#<owning-phase-or-step> in the bead body (and, when the tracker supports it, tbd update <bead-id> --spec <spec-path>),
- for each blocker:
tbd dep add <bead-id> <blocker-id>.
--label epic:<slug> is the canonical grouping — the label, not the parent link, is the join key /substrate:synthesize-session and bead-graph.sh rely on. --parent is the nicety on top. group:<window-N> is the partition membership the orchestrator reads (and MAY re-batch) per agents-parallel-execution-doctrine.md §Grouping & windows. doctrine:<id> is the graph-time binding dispatchers push on: the orchestrator inlines the window's digests via doctrine-digest.sh <id>, and a daemon lane reads the label-named doctrines first.
- Do not
tbd sync here — batch sync stays the orchestrator's call at epic close (parallel-execution doctrine, Policy 3).
Branch B — none: write each bead to docs/tasks/ongoing/<slug>/beads/<bead-slug>.md with blocked-by:, epic: <slug>, group: <window-N> (its Step-4.5 window), doctrine: [<id>, …] (its Step-4.55 bindings — omit the key entirely when there are none), and spec: <spec-path>#<owning-phase-or-step> in frontmatter. The markdown file is the bead.
Step 6 — Show the shape
Render the DAG so the user (and any future agent) can see parallel vs. sequential structure:
bash docs/scripts/bead-graph.sh --epic <slug> # topological waves (default)
bash docs/scripts/bead-graph.sh --epic <slug> --format mermaid # paste-able graph TD
Print the waves view inline. Under tbd, the script reads the beads you just created; under none, tell the user the waves view requires tbd and print the DAG from your in-memory layering instead.
Step 7 — Hand off
Print verbatim:
✔ Spec graphed into a bead DAG.
Epic label: epic:<slug> · 1 epic + N beads · W waves (see above)
Inspect any time:
bash docs/scripts/bead-graph.sh --epic <slug>
bash docs/scripts/bead-graph.sh --epic <slug> --format mermaid
To execute in parallel, hand this DAG to the orchestrator per
docs/doctrine/agents-parallel-execution-doctrine.md (single-writer tracker,
integration branch, file-disjoint waves). For a simple sequential pass:
claude /substrate:execute docs/tasks/ongoing/<slug>/<slug>-spec.md
Constraints
- MUST derive
<slug> from the spec directory and tag every bead (epic + children) with epic:<slug>. The label is the canonical epic identity — it is the contract with /substrate:synthesize-session and bead-graph.sh. Do not skip it.
- MUST emit exactly one terminal doctrine-reconciliation node per epic (Step 4.6): tagged
kind: doctrine-reconciliation, blocked-by every other bead, docs/doctrine/** in its write-scope, in its own solo terminal group:<window-N>. This is the in-epic sink for doctrine change — the graph MUST NOT rely on a downstream doctrine-amendment queue.
- MUST cycle-check via Kahn before persisting and REFUSE on a cycle. A cyclic "DAG" is a bug, not a plan.
- MUST be the single writer to tbd (parallel-execution doctrine Policy 1). Never hand the tbd CLI or
git push to a subagent.
- MUST NOT
tbd sync — batch sync is the orchestrator's, at epic close.
- MUST NOT execute the beads, write feature code, or open worktrees. This skill only produces the graph.
- MUST NOT double-create: if an epic for
<slug> already exists, render the existing DAG and offer to add only missing beads.
- MUST clean up every tempfile it renders (
unlink even on partial failure).
- MUST run the invalidated-tests reverse-scan for every behavior-changing bead and fold the affected existing test files into that bead's Files/write-scope with a
reconcile: note. A behavior change whose broken test sits outside the bead's Files is a guaranteed mid-run stall — the group-runner can't touch what isn't in scope.
- MUST stamp
doctrine:<id> (Step 4.55) on every bead whose write-scope intersects a manifest entry's paths: globs, using the manifest's id verbatim so doctrine-digest.sh <id> resolves it downstream — and MUST leave the terminal kind: doctrine-reconciliation node unstamped. When no manifest exists or no entry declares paths:, the step is a silent no-op — never a warning, never an abort.
- MUST tag a bead
gate-scope: partial when its inlined gate is a strict subset of substrate.yaml's gate.* (or omits a suite its layer is covered by), so the orchestrator treats the per-bead green as a pre-check and relies on the wave's union re-gate to authorize the merge.
- SHOULD keep bead granularity at one-step-one-bead unless steps are file-coupled and share a Verify block — over-splitting inflates the DAG, under-splitting kills parallelism.
1---2name: graph-spec3description: Graph the Spec — decompose a written SDD spec into a directed acyclic graph of tbd beads so it can be executed in parallel. Invoke with a spec path (docs/tasks/ongoing/<slug>/<slug>-spec.md), or run with no args to discover the ongoing spec. Parses the spec's Prompt Execution Strategy (phases → steps), turns each unit into a bead, infers blocked-by edges from which files/symbols a step consumes vs. creates (folding behavior-invalidated tests into the changing bead's write-scope), cycle-checks via Kahn, partitions the DAG into context-budget windows by file-adjacency (group:<window-N> labels — the unit the orchestrator dispatches), stamps doctrine:<id> labels where a bead's write-scope intersects the doctrine manifest's paths: globs, and persists an epic + child beads under the canonical label epic:<slug>. Prints the wave shape via docs/scripts/bead-graph.sh. Called automatically as architect-spec's final step, or standalone to (re)graph any existing spec. Produces the DAG only — the parallel-execution doctrin4---56# /substrate:graph-spec78Turn a finished spec into a **bead DAG**: one epic + child beads wired by `blocked-by:` edges, all tagged with the canonical epic label `epic:<slug>`. The DAG is the input to substrate's parallel-execution doctrine — beads in the same topological wave touch no shared blocker and can be dispatched to worktree-isolated subagents at once.910This skill **produces the graph; it does not run it.** Execution is the parallel-execution orchestrator's job (`docs/doctrine/agents-parallel-execution-doctrine.md`), or a plain phase-by-phase `/substrate:execute` pass.1112## Arguments1314`<spec-path>` — path to a spec at `docs/tasks/ongoing/<slug>/<slug>-spec.md`. If omitted, discover it (see Step 1).1516## When to run1718- A spec exists and its Prompt Execution Strategy section is filled (phases → steps → verify → gate).19- You want the work decomposed into independently-dispatchable beads before execution — especially when phases contain file-disjoint steps that could run in parallel.20- Invoked automatically as the last step of `/substrate:architect-spec`, or by hand on any existing spec.2122## When to REFUSE2324Fail fast — abort with a one-line explanation, never fall back to a guess.2526| Signal | Action |27|--------|--------|28| No spec path given AND none discoverable under `docs/tasks/ongoing/**/*-spec.md` | Ask the user for the spec path. |29| Spec has no "Prompt Execution Strategy" section (per `execution-format.md`) | Abort: "This spec has no execution strategy to decompose. Run `/substrate:architect-spec` to produce one." |30| Bead-tracker unresolvable (see Step 2) | Abort: state whether `tbd` or a markdown fallback is expected, and why neither resolved. |31| An epic already exists for this slug (`tbd list --type epic --label epic:<slug>` non-empty) | Do NOT double-create. Skip to Step 6 and render the existing DAG; offer to add only the missing beads. |3233## Protocol you operate under3435- **Execution grammar**: `docs/protocol/sdd/execution-format.md` (phases → steps → verify → gate) — the structure you decompose.36- **DAG algorithm**: mirrors `/substrate:synthesize-session` Step 8 (pairwise blocked-by inference + Kahn cycle detection). Same "bead DAG" dialect on purpose — one graph vocabulary across the plugin.37- **Consumer**: `docs/doctrine/agents-parallel-execution-doctrine.md` (single-writer tracker, integration branch, file-disjoint waves).3839## Workflow4041### Step 1 — Resolve the spec4243If a path was passed, use it. Else glob `docs/tasks/ongoing/**/*-spec.md`:44- exactly one match → use it;45- several → list them and ask which;46- none → REFUSE per the table.4748Derive `<slug>` from the containing directory name (`docs/tasks/ongoing/<slug>/...`). That slug is the whole coordination key: the epic label is `epic:<slug>`, deterministically, so `/substrate:synthesize-session` later files follow-up beads under the *same* label without any handshake.4950Read the spec. Confirm it has a Prompt Execution Strategy section; if not, REFUSE.5152### Step 2 — Resolve the bead-tracker5354Same resolution as `/substrate:synthesize-session` (keep them identical):55561. If `.substrate/config.json` sets `"bead-tracker"`, honor it (`tbd | none | other`).572. Else auto-detect `tbd`: `.tbd/config.yml` exists AND a `tbd` binary is callable — `command -v tbd` succeeds, OR `npx --no-install get-tbd --version` succeeds. Else `none`.5859- `tbd` → beads are canonical; this skill creates them via `tbd create`. Substitute `npx --no-install get-tbd` for `tbd` throughout if no global binary is on `PATH`.60- `none` → markdown is canonical; beads live at `docs/tasks/ongoing/<slug>/beads/<bead-slug>.md`. `blocked-by:` in frontmatter is the only edge record (no `tbd dep`).6162### Step 3 — Decompose the spec into bead candidates6364Walk the Prompt Execution Strategy. Each **step** (`#### Step N.M`) is one bead candidate; collapse trivially-coupled sibling steps only when they edit the same file and share a single Verify block. For each candidate capture, in memory (no writes yet):6566- **title** — imperative, scoped (e.g. "Add `requireStoreOwner` guard to `convex/stores.ts`").67- **phase / layer** — the owning phase and its `layer-hint` (domain / backend / frontend / infra / cross-cutting).68- **creates** — files, symbols, exports, tables the step introduces or modifies (read the step body + its Verify block).69- **consumes** — files/symbols/commands the step depends on existing.70- **invalidated tests (reconcile)** — existing tests that assert behavior this bead *changes* (distinct from any new tests the step adds). A behavior change to an existing symbol / contract / exported member / required-dependency almost always breaks a test that pinned the old behavior, yet the step body rarely names that test. **Reverse-scan the test tree** for tests referencing the changed symbol/file/contract and fold each matching test file into this bead's **creates** (its write-scope) with a one-line `reconcile:` note ("asserts prior behavior of X"). Skipping this is the single most common cause of a mid-run stall: the test lives outside the bead's declared Files, so the group-runner can't touch it and the break surfaces only at the integrated re-gate. Purely-additive beads (new files, no change to an existing symbol) have no invalidated tests — leave this empty.71- **gate** — the step's Verify commands, inlined (a subagent runs these; per the parallel-execution doctrine it never touches tbd or git). Compare it to `substrate.yaml`'s `gate.{compile,test,lint}`: if the bead's gate is a **strict subset** of `gate.*`, or omits a suite the bead's layer is actually covered by (e.g. a frontend bead that runs only `tsc`, not the `vitest` suite), tag the bead **`gate-scope: partial`** in its body. That tells the orchestrator its per-bead green is a *fast pre-check only*, and that the wave's **union re-gate** — not this narrow gate — authorizes the merge (`agents-parallel-execution-doctrine.md §Supporting → Re-run the gate on the integrated branch`). A gate that equals or supersets `gate.*` needs no tag.72- **acceptance criterion** — binary pass/fail lifted from the step + its gate.7374### Step 4 — Build the DAG7576Mirror `/substrate:synthesize-session` Step 8:77781. **Pairwise scan.** For each ordered pair `(A, B)`, mark `B blocked-by A` iff B's **consumes** references a file/symbol/export/table in A's **creates**. This is richer than the linear phase order: two steps in the same phase that touch disjoint files land with no edge between them → same wave → parallel. Record a one-line reason per edge.792. **Layer backstop.** If two beads share a file in their **creates** set, serialize them with an edge (later phase blocked-by earlier) even absent a symbol dependency — the parallel-execution doctrine forbids two beads editing one file in a wave.803. **Cycle detection (Kahn).** Compute in-degrees; peel zero-in-degree nodes. If any remain → a cycle exists. **REFUSE**: print the cycle (`A → B → C → A`) and ask the user to split or drop a bead. Never emit a cyclic DAG.814. Encode `blocked-by:` on each bead.8283### Step 4.5 — Partition into context-budget windows8485The DAG is now cycle-free; cut it into agent-sized **windows** so no group-runner's context86rots or auto-compacts. Read `execution.context-budget` from `substrate.yaml` (default `0.4` if87absent — a deviatable prior, not a hard gate). Then:88891. **Estimate per-bead cost.** Walk beads in topological order. For each, estimate90 `cost = Σ(bytes of its Files/creates+consumes) + heavy-ref surcharge (schema / contract /91 migration reads the bead must load) + gate-log weight + effort(XS…L)`. This is a heuristic92 prior, not a measurement — round generously.932. **Accumulate into windows.** Open `window-1`; add beads in topological order, summing cost.94 When the running total would cross `context-budget` (as a fraction of one agent's usable95 window), close the current window and open the next. **Snap boundaries to file-adjacency:**96 never split a chain of co-edited beads (overlapping `Files`) across two windows — co-edited97 beads share a warm worktree, so they belong to the *same* window; file-disjoint chains fall98 into *separate* windows (isolation, parallel where edges allow). Adjacency wins over the raw99 cost cut when they conflict.1003. **Flag under-decomposition.** If a *single* bead's cost alone exceeds `context-budget`, do101 **not** silently over-fill a window: **warn** and recommend splitting that bead (it is too102 heavy for one runner), then place it in its own window so the run can still proceed.103104The partition is a **deviatable prior**: the orchestrator MAY re-batch at dispatch time (logging105the deviation). See `agents-parallel-execution-doctrine.md §Grouping & windows`.106107### Step 4.55 — Bind doctrine to write-scope (`doctrine:<id>` labels)108109Windows exist and every bead has a write-scope. Bind each bead to the doctrines that **govern the110files it will edit**, at graph time, so a dispatcher can *push* the right doctrine into the111worker's prompt instead of hoping the worker goes looking. Only the manifest's `paths` key answers112the file question — `triggers` matches a *brief* (prose) at spec time, `paths` matches a113*write-scope* (files) at graph time (`docs/doctrine/agents-doctrine.md` §3.1).1141151. **Read the manifest** at `docs/doctrine/doctrine-manifest.yaml`. Take each entry's `id` and its116 optional `paths:`, parsed in **inline-list form only** — `paths: [docs/doctrine/**, x/y.sh]`, one117 line — exactly how `doctrine-lint.sh` parses `pointers`. A block sequence (`paths:` then `- a/**`)118 is not the declared form and the tooling cannot read it: report it as a manifest bug, do not119 accommodate it.1202. **Silent no-op when there is nothing to bind.** No manifest, or no entry declares `paths:` →121 stamp nothing and say nothing. Not a warning, not an abort: substrate must keep graphing specs in122 repos that never adopted the key, and a warning would fire on every run in every one of them.1233. **Intersect.** A bead's write-scope is **creates ∪ modifies** — Step 3's `creates` set, which has124 already absorbed the invalidated-test files. `consumes` is read-only and does **not** bind. A bead125 is bound to `<id>` iff any path in its write-scope matches any glob in that entry's `paths:`.126 Match relative to the repo root and **permissively — a `*` may cross `/`** (the same reading127 `doctrine-lint.sh` uses for its rot guard), so `docs/doctrine/**` matches128 `docs/doctrine/agents-doctrine.md`, and a bead whose declared scope is itself a directory glob129 (`convex/**`) matches a doctrine glob rooted there.1304. **Stamp `doctrine:<id>`** — one label per matching entry; a bead may carry zero, one, or several.131 `<id>` is the manifest's **`id` field verbatim** (not the filename, not the path). It is the key132 downstream consumers resolve with `bash docs/scripts/doctrine-digest.sh <id>`; any other spelling133 breaks that chain silently.1345. **Exempt the terminal node.** The doctrine-reconciliation bead Step 4.6 shapes (the spec's135 `Phase N: Doctrine Reconciliation` step, or the node you synthesize when the spec lacks one) gets136 **no** generic `doctrine:<id>` labels — even though its `docs/doctrine/**` write-scope would match137 every doctrine that governs doctrine files. Its whole brief *is* reconciliation against the fully138 integrated epic; stamping it would bind it to a near-arbitrary subset and drown the signal. Step139 4.6's contract is the only one it carries.140141Worked example, against this repo's manifest (`agents` → `paths: [docs/doctrine/**,142docs/scripts/doctrine-lint.sh]`; `agents-parallel-execution` → `paths: [docs/scripts/bead-graph.sh]`):143144| bead | write-scope | labels stamped |145|---|---|---|146| Add rule 6 to the linter | `docs/scripts/doctrine-lint.sh` | `doctrine:agents` |147| Teach `bead-graph.sh` a flow view | `docs/scripts/bead-graph.sh` | `doctrine:agents-parallel-execution` |148| Document `paths:` in §3 | `docs/doctrine/agents-doctrine.md`, `docs/doctrine/doctrine-manifest.yaml` | `doctrine:agents` (via `docs/doctrine/**`) |149| Rename a Convex table | `convex/schema.ts` | *(none — no entry's `paths:` covers it)* |150| **Terminal** `kind: doctrine-reconciliation` | `docs/doctrine/**` | **none — exempt per (5)**, though the globs would match |151152### Step 4.6 — Force the terminal doctrine-reconciliation node153154Every spec ends with `Phase N: Doctrine Reconciliation` (per `spec-template.md`). Its bead is **not** an ordinary node — it is the epic's mandatory **terminal** node that applies the ratify-only doctrine change the feature earned, in-epic. It never queues a `doctrine-amendment` and there is no downstream sink. Shape it explicitly, overriding the generic decomposition:1551561. **Tag it** `kind: doctrine-reconciliation` (an additive tag alongside `epic:<slug>`), so the orchestrator can identify the terminal node.1572. **Edge it `blocked-by` every other bead in the epic.** It must see the *fully integrated* feature — no other bead may land after it. This is deliberately the one node that collapses parallelism at the end; that's correct.1583. **Write-scope = `docs/doctrine/**`.** Put `docs/doctrine/**` in its `creates`/Files. Editing doctrine files is ordinary working-tree change a group-runner may make in its worktree — it is **not** a tracker write, so the single-writer invariant is untouched.1594. **Solo terminal window.** Give it its own `group:<window-N>` as the last window (it is `blocked-by` everything, so it is structurally its own final wave regardless). Never fold it into a feature window.1605. **Ratify-only gate.** Its gate is the epic's full union gate re-run on the integrated tip — green proves the mutation only codified what the code already did. Do **not** tag it `gate-scope: partial`.161162If the spec somehow lacks a Doctrine Reconciliation phase, **synthesize this node anyway** — it is mandatory per the contract; warn that the spec was missing it.163164### Step 5 — Persist the epic + beads165166Preview the full bead list in DAG order inline, then ask: `Create 1 epic + N beads under epic:<slug> now? (Y / n / select)` — default `Y` (binary gate, no default-escape suffix). `select` enters a per-bead `y / n / skip` loop.167168**Branch A — `tbd`** (single-writer; you are the only writer):1691701. **Epic bead:** `tbd create "Epic: <spec title>" --type epic -l "epic:<slug>" --file <spec-ref>` (a tempfile holding the spec path + one-line summary; `mktemp`, unlink after). Capture its id as `<epic-id>`.1712. **Child beads,** in DAG order so `blocked-by:` resolves to already-assigned ids:172 - render the bead body (acceptance criterion + inlined gate + state-transfer prompt) to a tempfile,173 - `tbd create "<title>" --type task --parent <epic-id> -l "epic:<slug>" -l "group:<window-N>" [-l "doctrine:<id>" …] --file <tmp>`, where `<window-N>` is the bead's window from Step 4.5 and the zero-or-more `doctrine:<id>` flags are its Step-4.55 bindings, one `-l` each (add `-l "kind:doctrine-reconciliation"` for the terminal node from Step 4.6 — which carries no `doctrine:` labels); capture the id, `unlink` the tempfile (unconditional cleanup, even on failure),174 - stamp the spec back-link so a cold runner can re-open context: include `spec: <spec-path>#<owning-phase-or-step>` in the bead body (and, when the tracker supports it, `tbd update <bead-id> --spec <spec-path>`),175 - for each blocker: `tbd dep add <bead-id> <blocker-id>`.1763. `--label epic:<slug>` is the canonical grouping — the label, not the parent link, is the join key `/substrate:synthesize-session` and `bead-graph.sh` rely on. `--parent` is the nicety on top. `group:<window-N>` is the partition membership the orchestrator reads (and MAY re-batch) per `agents-parallel-execution-doctrine.md §Grouping & windows`. `doctrine:<id>` is the graph-time binding dispatchers push on: the orchestrator inlines the window's digests via `doctrine-digest.sh <id>`, and a daemon lane reads the label-named doctrines first.1774. Do **not** `tbd sync` here — batch sync stays the orchestrator's call at epic close (parallel-execution doctrine, Policy 3).178179**Branch B — `none`:** write each bead to `docs/tasks/ongoing/<slug>/beads/<bead-slug>.md` with `blocked-by:`, `epic: <slug>`, `group: <window-N>` (its Step-4.5 window), `doctrine: [<id>, …]` (its Step-4.55 bindings — omit the key entirely when there are none), and `spec: <spec-path>#<owning-phase-or-step>` in frontmatter. The markdown file is the bead.180181### Step 6 — Show the shape182183Render the DAG so the user (and any future agent) can see parallel vs. sequential structure:184185```bash186bash docs/scripts/bead-graph.sh --epic <slug> # topological waves (default)187bash docs/scripts/bead-graph.sh --epic <slug> --format mermaid # paste-able graph TD188```189190Print the waves view inline. Under `tbd`, the script reads the beads you just created; under `none`, tell the user the waves view requires `tbd` and print the DAG from your in-memory layering instead.191192### Step 7 — Hand off193194Print verbatim:195196```197✔ Spec graphed into a bead DAG.198199Epic label: epic:<slug> · 1 epic + N beads · W waves (see above)200201Inspect any time:202 bash docs/scripts/bead-graph.sh --epic <slug>203 bash docs/scripts/bead-graph.sh --epic <slug> --format mermaid204205To execute in parallel, hand this DAG to the orchestrator per206docs/doctrine/agents-parallel-execution-doctrine.md (single-writer tracker,207integration branch, file-disjoint waves). For a simple sequential pass:208209 claude /substrate:execute docs/tasks/ongoing/<slug>/<slug>-spec.md210```211212## Constraints213214- MUST derive `<slug>` from the spec directory and tag every bead (epic + children) with `epic:<slug>`. The label is the canonical epic identity — it is the contract with `/substrate:synthesize-session` and `bead-graph.sh`. Do not skip it.215- MUST emit exactly one **terminal doctrine-reconciliation node** per epic (Step 4.6): tagged `kind: doctrine-reconciliation`, `blocked-by` every other bead, `docs/doctrine/**` in its write-scope, in its own solo terminal `group:<window-N>`. This is the in-epic sink for doctrine change — the graph MUST NOT rely on a downstream `doctrine-amendment` queue.216- MUST cycle-check via Kahn before persisting and REFUSE on a cycle. A cyclic "DAG" is a bug, not a plan.217- MUST be the single writer to tbd (parallel-execution doctrine Policy 1). Never hand the tbd CLI or `git push` to a subagent.218- MUST NOT `tbd sync` — batch sync is the orchestrator's, at epic close.219- MUST NOT execute the beads, write feature code, or open worktrees. This skill only produces the graph.220- MUST NOT double-create: if an epic for `<slug>` already exists, render the existing DAG and offer to add only missing beads.221- MUST clean up every tempfile it renders (`unlink` even on partial failure).222- MUST run the **invalidated-tests reverse-scan** for every behavior-changing bead and fold the affected existing test files into that bead's Files/write-scope with a `reconcile:` note. A behavior change whose broken test sits outside the bead's Files is a guaranteed mid-run stall — the group-runner can't touch what isn't in scope.223- MUST stamp `doctrine:<id>` (Step 4.55) on every bead whose write-scope intersects a manifest entry's `paths:` globs, using the manifest's `id` verbatim so `doctrine-digest.sh <id>` resolves it downstream — and MUST leave the terminal `kind: doctrine-reconciliation` node unstamped. When no manifest exists or no entry declares `paths:`, the step is a **silent no-op** — never a warning, never an abort.224- MUST tag a bead **`gate-scope: partial`** when its inlined gate is a strict subset of `substrate.yaml`'s `gate.*` (or omits a suite its layer is covered by), so the orchestrator treats the per-bead green as a pre-check and relies on the wave's union re-gate to authorize the merge.225- SHOULD keep bead granularity at one-step-one-bead unless steps are file-coupled and share a Verify block — over-splitting inflates the DAG, under-splitting kills parallelism.