/substrate:synthesize-session
Capture the session's learning before it evaporates. Convert it into doctrine fixes, dependency-ordered beads, and parked open-design-questions — each shaped so a fresh agent can act on it without re-reading the originating session. Doctrine changes the epic earned were already applied in-epic by the executor's terminal Doctrine Reconciliation phase (ratify-only); this skill does not queue amendments.
Position in the lifecycle
/substrate:architect-spec → /substrate:orchestrate → /substrate:synthesize-session
(plan) (build + commit + (capture learning →
in-epic doctrine reconcile) beads + parked questions)
[primary: parallel fleet]
or /substrate:execute (attended single-window)
Not a phase of the executor. The build step is /substrate:orchestrate (the primary door — a
parallel worktree fleet over the graphed DAG) or its attended single-window alternative
/substrate:execute; synthesize runs after either one lands the feature. Per-feature idempotent — if
you built three specs this session you can synthesize each one independently. A re-run on the same
feature either no-ops (status: complete in state file) or resumes (status: in-progress).
What this skill reads
This skill operates on the model's own context window. There is no transcript file to consult. If the context has been auto-compacted, older session learning is gone — the synthesis will be degraded. Step 0 detects this and warns.
In addition to context, the skill reads:
git log <pre-session-base>..HEAD— what shipped.docs/doctrine/**/*.md— the drift target.- Existing beads (via the configured bead-tracker; see "Bead-tracker config" below).
.substrate/synthesis-state.json(if present) — per-feature idempotency + resumability state.docs/synthesis-index.md(if present) — past syntheses' bead inventories, for cross-session dedup.
Arguments
[feature] (optional) — feature slug whose spec was just executed. If omitted, the skill auto-detects the most recently archived feature (definition in Step 1). If multiple are tied, the skill asks the user to pick one — ending the question with [type 'default' to let me decide sensible defaults].
When to run
- A spec was just built in this session and archived to
docs/tasks/completed/<feature>/by the executor —/substrate:orchestrate(the primary parallel door) or the attended/substrate:execute. - You are still in the same Claude session as that build — the model's working context is the primary input.
When to REFUSE
| Signal | Redirect |
|---|---|
No docs/doctrine/ directory |
Not a scaffolded substrate project. Run /substrate:init first. |
No feature exists at docs/tasks/completed/<feature>/ |
Nothing to synthesize. Did /substrate:orchestrate (or the attended /substrate:execute) complete? |
.substrate/synthesis-state.json[<feature>].status === "complete" |
Already synthesized — print the narrative-commit SHA and exit (idempotent). |
.substrate/synthesis-state.json[<feature>].status === "in-progress" |
Resume rather than refuse: jump to the step after the last entry in completed-steps:. |
Legacy docs/tasks/completed/<feature>/synthesis-*.md exists with status: complete (pre-Option-A) |
Treat as already-synthesized — print the legacy path and exit (no migration). |
Working tree has >2 modified or staged files outside docs/, scripts/dev*/, or paths the originating spec touched |
Stop. Mixing synthesis commits with unrelated WIP corrupts bisectability. Ask the user to stash or commit first. |
Bead-tracker config
The skill resolves the bead-tracker as follows:
- If
.substrate/config.jsonexists and contains"bead-tracker": "tbd" | "none" | "other", honor it. - Otherwise auto-detect:
tbdif.tbd/config.ymlexists AND atbdbinary is callable — eithercommand -v tbdsucceeds (global install, e.g.pnpm add -g tbd) ORnpx --no-install get-tbd --versionsucceeds (local install). Elsenone.
Canonical store depends on tracker:
- If tracker is
tbd: tbd is canonical. Bead bodies are composed in memory in Step 7, previewed inline to the user, and persisted in Step 9 via ephemeral tempfiles (mktempunder/tmpor$TMPDIR) passed totbd create --file. The tempfile is unlinked immediately after each create succeeds. The working tree never gets a markdown file. Provenance lives in (a) the synthesis-complete commit body which lists the assigned tbd IDs, (b) theoriginating-spec/originating-sessionfrontmatter inside each tbd record. - If tracker is
none: markdown is canonical. Beads live permanently atdocs/tasks/ongoing/<bead-slug>/bead.md(mirrors the spec convention — directories not bare files). The markdown file IS the bead.
State file
Resumability + idempotency live in .substrate/synthesis-state.json. Create the file with {} if missing. Schema:
{
"<feature>": {
"status": "in-progress" | "complete",
"started": "<ISO8601>",
"completed": "<ISO8601 — only when status: complete>",
"completed-steps": [4, "4b", 5, 6, "6.5", 7, 8, 9],
"context": "full" | "compacted",
"narrative-commit": "<sha — backfilled after final commit>"
}
}
.substrate/synthesis-state.json SHOULD be committed (it's the per-feature synthesis ledger; without it a fresh clone loses synthesis history).
Workflow
Step 0 — Compaction self-check
For each commit hash in git log --oneline <base>..HEAD, the model self-assesses: "do I recall why this commit was made, beyond its message?" If recall is sparse on more than 50% of session commits, print:
⚠ Context appears compacted. Synthesis quality will be degraded.
Recall sparse on N / M session commits.
Recommended: re-run in a fresh session with the spec attached.
Proceed anyway? (y / n)
If the user proceeds anyway, the state file's context field MUST record compacted so downstream readers know this synthesis was degraded.
Step 1 — Detect session base + feature
Run in parallel:
ls docs/tasks/completed/ # candidate features
git log --diff-filter=ACDMR -M --name-only --format='%h %ct %s' \
-- 'docs/tasks/completed/**' 'docs/tasks/ongoing/**' # archive activity (handles renames)
git status --porcelain # working-tree cleanliness
git log -1 --since='24 hours ago' --format='%H' 2>/dev/null || true # session window
Most recently archived feature = the feature directory under docs/tasks/completed/ whose latest commit-timestamp (git log -1 --format=%ct -- <dir>) is the maximum among directories touched within the last 24 hours OR since the last git push, whichever window is longer.
If exactly one candidate → use it. If multiple → present them with timestamps and ask the user (with default-escape suffix). If zero → REFUSE.
<base> := parent of the earliest commit in this session window that touched either docs/tasks/completed/<feature>/ or docs/tasks/ongoing/<feature>/.
Step 2 — Gather inputs + compute already-actioned set
Run in parallel:
git log --format='%h %s' <base>..HEAD # session commits
git diff --stat <base>..HEAD # session blast radius
git log --name-only --format= <base>..HEAD -- docs/doctrine/ # already-actioned doctrine files
find docs/doctrine -type f -name '*.md' # doctrines to scan
test -f .substrate/synthesis-state.json && cat .substrate/synthesis-state.json # resumability state
test -f .substrate/config.json && cat .substrate/config.json # explicit tracker config
test -f .tbd/config.yml && (command -v tbd >/dev/null || npx --no-install get-tbd --version) # tbd auto-detect (global or local)
test -f docs/synthesis-index.md && cat docs/synthesis-index.md # cross-session inventory
Already-actioned filter (G1): the list of doctrine files touched between <base> and HEAD defines the already-shipped set. Step 3 MUST exclude any candidate whose target file (or specific section within file, when grep-resolvable) is in that set. Re-suggesting a fix that already landed this session is a critical failure.
Bead inventory: if tracker is tbd, run tbd list if a global binary is on PATH, else npx --no-install get-tbd list; otherwise enumerate docs/tasks/ongoing/<*>/bead.md. Also load every bead row from docs/synthesis-index.md if it exists (G10 cross-session dedup).
Initialize state file. Before Step 4 begins, write .substrate/synthesis-state.json with:
"<feature>": {
"status": "in-progress",
"started": "<ISO8601 now>",
"completed-steps": [],
"context": "full" | "compacted"
}
(Create .substrate/ directory if missing.)
Step 3 — Scan and categorize (draft, no writes yet)
Walk the model's context and produce a draft candidate list spanning the seven categories:
| Category | Definition |
|---|---|
| DevX (humans) | Ergonomics for the developer — scripts, orchestrators, shortcuts |
| DevX (agents) | Token / cycle reduction — pre-flights, shared libraries, smaller prompts |
| Bugs not seen | Latent bugs the session implicitly revealed but didn't trigger |
| Implementation drift | Code that drifted from its doctrine prescription |
| Architectural drift | Doctrine that drifted from reality (the claim is now wrong) |
| Doctrine gap (missing axis) | An architectural axis the session introduced that no existing doctrine governs — absence, not drift (e.g. the epic added infra/ but there is no infra-doctrine.md) |
| Feature extensions | Net-new behaviour or optimisations surfaced by the session |
Empty categories are signal too — explicitly note them.
Tag each candidate as one of five buckets:
- immediate-fix — single file, factual, trivial revert, would mislead the next session within hours. Caps at 5; surplus demotes to deferred-fix.
- deferred-fix — same shape as immediate-fix, but past the cap. Filed as a
type: driftbead, not as an amendment (preserves the trivial revert shape). - missing-doctrine — an architectural axis the session introduced with no governing doctrine file. Authored as a session-filled draft doctrine in Step 4b (cap 3; surplus demotes to a
type: featurebead recommending the axis). - bead — net new work to be done. This bucket now absorbs non-ratify-only doctrine follow-ups: a doctrine change that would require changing already-shipped code (a new MUST/MUST-NOT the epic's landed code violates) is out of scope for the epic's terminal reconciliation node (which is ratify-only), so it lands here as an actionable
type: taskbead describing the stricter rule + the code it obliges — never as a passivedoctrine-amendmentqueue entry. Ratify-only doctrine changes are not candidates here at all: they were already applied in-epic by the Doctrine Reconciliation phase. - parked-question — open design question with no doctrine claim yet, no committed acceptance criterion. Filed as a
type: open-question,status: parkedbead so it shows up in the tracker but isn't pulled into the DAG as actionable work.
Apply the already-actioned filter. Apply the cross-session dedup against past beads.
Also draft, in memory, two synthesis-level artifacts:
- §1 session narrative — two paragraphs max: what was specced + what shipped + the most consequential design call. Will land in the final synthesis-complete commit body.
- §7 Pareto cut — the top 3–5 leverage items across the DAG (ignoring dependency order). Same constraint as Step 4's leverage ranking. Will print to chat in Step 10 and also land in the synthesis-complete commit body.
Present the draft. Wait for the user: y / modify / defer <id-or-csv>. defer drops items from this run.
Step 4 — Apply immediate doctrine fixes (cap = 5)
Leverage ranking (F8): for each immediate-fix candidate, score 1–5 on miscoaching_cost × inverse_revert_cost:
miscoaching_cost(1–5): if the next agent reads the doctrine as-is, how badly does it lead them astray? 5 = produces broken code; 1 = cosmetic.inverse_revert_cost(1–5): how easy is it to revert this fix if wrong? 5 = single line, single file; 1 = touches multiple files / cascades.
Sort descending. Top 5 land as immediate fixes; rest demote to deferred-fix (Step 7, as type: drift beads).
For each of the top 5:
- Re-verify inclusion criteria: single file, factual, trivial revert.
- Make the edit.
- Commit as its own atomic commit. Honor the project's commit-message convention — check recent commits (
git log -10 --format=%B) for trailers likeCo-Authored-By:orSigned-off-by:and replicate the pattern. Do not impose a substrate-specific convention.
After each commit, update .substrate/synthesis-state.json[<feature>].completed-steps: to include 4 (idempotent — once 4 is present, leave it). This is what makes a mid-step crash resumable.
Step 4b — Author missing doctrines (filled drafts, cap = 3)
Step 4 repairs doctrines that exist. Step 4b handles the opposite failure: an architectural axis the session introduced that no doctrine governs at all. A 5-wave, 18-bead epic that stands up new infra or a new runtime subsystem should leave behind N new doctrine files — and this session, the one that knows the real rules, is the only cheap moment to write them. Deferring to a human running /substrate:add-doctrine cold next week just yields a placeholder stub, because the context that would fill it has evaporated.
Detection — coverage map (not drift). A missing axis is an absence, distinct from Step 4/5's wrong claim. Compute it:
- Touched areas — from
git diff --stat <base>..HEAD, take the first path segment of each changed file (e.g.convex/,src/,infra/, a new subsystem dir). Collapse to distinct areas. - Governed areas — for each existing doctrine, read its
## 1. Scope(or the manifestsummary+layer-hint) and map it to the area(s) it claims. The baseline three coverdomain(pure TS),backend(convex/),frontend(src/). missing = touched-areas \ governed-areas. Each remaining area with non-trivial blast radius (≥1 shipped commit's worth of code, not a lone config tweak) is a missing-doctrine candidate.
Because a governed area never enters missing, this subsumes the already-actioned filter for this step — no separate pass needed.
Derive the writer inputs (no Socratic Q&A — synthesis already knows every answer add-doctrine's Q1–Q5 would ask):
id— kebab-case name for the axis (newinfra/→infra; a runtime subsystem → its name).path— from the project's detected nesting convention (reuse add-doctrine Step 1's flat/nested/mixed detection).name— title-case(id).summary— one sentence describing what the axis governs, mined from the session.layer-hint— the coverage-map layer the gap sits in.triggers— 3–8 keywords mined from the epic's beads + the area's filenames.
Fill the sections from session context — this is the whole reason to author here rather than defer:
## 1. Scope— in/out of scope, grounded in what the session actually built.## 2. Binding Rules (MUSTs)— the hard rules the session's code established or revealed.## 3. Recommended Practices (SHOULDs)— patterns that demonstrably worked.## 4. Anti-patterns— anything the session tried and backed out of, or a footgun it hit.## 5. Examples— real file shapes / snippets from the shipped code.- Header stays
**Status**: Draft,**Last verified**: <today YYYY-MM-DD>— a session-filled starting point, not authority. No**Version**and no change log: a living doctrine carries current state only, and git owns its history.
Cap = 3. Rank candidates by blast radius (lines + commits touching the area). Top 3 are authored; any surplus demotes to a type: feature bead (Step 7) titled Author <id>-doctrine.md for the <area> axis, carrying the coverage-map evidence in its state-transfer prompt. Never dump >3 half-known doctrines in one run.
Gate — per candidate (y / modify / defer). Preview each filled draft inline, then ask per candidate:
y— write it.modify— apply the user's edits, re-preview.defer— drop from this run (does not auto-demote to a bead; the user chose to skip).
This is deliberately not the default-Y gate the bead-persistence step uses. A filled draft doctrine is the highest design surface synthesis touches, so each one earns an explicit yes.
Write + commit. For each approved candidate, apply /substrate:add-doctrine's Step 3 (write to the convention path) + Step 4 (manifest append / bootstrap) as the canonical writer — passing the filled sections + derived Q1–Q5 answers in place of the placeholder stub. The manifest dual-write keeps any manifest-coverage test green. Commit each as its own atomic commit:
doctrine(<id>): initial draft from <feature> session
Honor the project's commit-message convention (same git log -10 trailer inspection as Step 4).
After each commit, append "4b" to .substrate/synthesis-state.json[<feature>].completed-steps: (idempotent — once present, leave it).
Step 5 — Doctrine changes are reconciled in-epic (no amendment queue)
There is no amendment queue — no type: doctrine-amendment bead, no status: queued dead-letter, no file under docs/tasks/ongoing/doctrine-updates/. This step performs no persistence; it is retained as a numbered checkpoint so state-file completed-steps stays continuous. Doctrine change lands at two tiers, neither of them passive:
- Ratify-only changes — already applied. Any doctrine change the epic earned (a pattern the shipped code demonstrates, an outdated rule the code superseded, coverage the code exemplifies) was applied in-epic by the executor's terminal Doctrine Reconciliation phase (
spec-template.md§Phase N), inside the same diff as the feature, and re-gated green. Nothing to queue here. - Non-ratify-only follow-ups — actionable beads, not amendments. A doctrine change that would require changing already-shipped code (a stricter MUST/MUST-NOT the landed code violates) was correctly refused by the ratify-only terminal node, and lands instead as the Step-3
beadbucket's actionabletype: taskbead in Steps 7+9 — describing the stricter rule + the code it obliges to change — so a future session can do it rather than let it rot in a triage backlog.
Append 5 to .substrate/synthesis-state.json[<feature>].completed-steps:.
Step 6 — Annotate the archived spec (mandatory if deviations exist)
For any deviation between what the spec prescribed and what shipped, write a ### Post-execution notes block into docs/tasks/completed/<feature>/<feature>-spec.md.
Replace-not-append (F10): if a ### Post-execution notes heading already exists, rewrite its body in place (between that heading and the next heading or EOF). Never duplicate the block.
Commit as its own atomic commit: docs(<feature>): annotate post-execution deviations. After commit, append 6 to completed-steps.
Per SDD doctrine — see docs/protocol/sdd/_SPEC-STANDARD.md §11 Archive Protocol — this is the only sanctioned write to an archived spec. Step 6 is the canonical writer.
Step 6.5 — Doctrine usage report: bound vs cited (report-only)
/substrate:graph-spec Step 4.55 stamped doctrine:<id> on every bead whose write-scope intersected that doctrine's manifest paths:. Bindings are therefore labels rather than vibes, which makes doctrine EV measurable per epic: how many beads a doctrine was bound to, versus how many it was actually cited in when the work deviated or explained itself. A doctrine bound to many beads and cited in none is either mis-scoped (paths: too broad) or not saying anything the work needed — and nothing else in the lifecycle surfaces that asymmetry.
Report-only. Creates no beads, edits no doctrine, touches no manifest. Its entire output is the report, which lands in the Step-10 synthesis-complete commit body (no .md file — same convention as §1 and §7).
Bound (N). Enumerate the epic's beads (tbd list --label epic:<feature>), then read each one's labels with tbd show <id> | grep -oE 'doctrine:[[:alnum:]_-]+' — the same label read bead-graph.sh uses for group:. Tally beads per <id>. Under tracker none, read the doctrine: frontmatter key instead. If no bead carries a doctrine: label, the epic predates Step 4.55: say so in one line and skip the rest of the step (not a warning, not an abort).
Cited (M). A doctrine is cited by a bead when its manifest id, its filename (<id>-doctrine.md), or a doctrine-digest.sh <id> invocation appears in evidence attributable to that bead. The evidence this repo actually persists, and nothing else:
| Source | How to read it | Attribution |
|---|---|---|
| Merged commit bodies | .substrate/execution-state.json[<feature>].outcomes[<bead>].commit → git show -s --format=%B <sha> |
that bead |
### Post-execution notes |
the archived spec Step 6 just wrote — the deviation record | the beads it names; else epic-level |
| Reconciliation edits | git log --name-only --format= <base>..HEAD -- docs/doctrine/ — reuse Step 2's already-actioned set |
epic-level: an edit to <id>-doctrine.md cites <id> |
.substrate/runs/<feature>/<run-id>/ |
group-runner traces + deviation log, if still on disk | best-effort only — gitignored and TTL-swept, so absence is normal and never evidence of M=0 |
M = how many of that doctrine's N bound beads carry ≥1 attributable citation. Epic-level citations do not raise M — report them in their own column so a doctrine that was reconciled isn't misread as inert.
Emit the asymmetry first, above the table, one line per offender — this is the point of the metric, not a footnote in a table:
⚠ doctrine:agents-parallel-execution — bound to 7 beads, cited in 0.
Audit docs/doctrine/agents-parallel-execution-doctrine.md: is `paths:` too broad
(7 beads' write-scopes matched globs the doctrine has nothing to say about), or is
its §2 Binding Rules not carrying rules the work needed?
| doctrine | bound (N) | cited (M) | epic-level | read as |
|---|---|---|---|---|
| agents-parallel-execution | 7 | 0 | — | ⚠ audit `paths:` or content |
| agents | 3 | 3 | reconciled | earning its binding |
| backend | 2 | 1 | — | partial |
Fire the callout when M = 0 and N ≥ 3 — below 3 the sample is too thin to accuse a doctrine, and the noise would train the reader to skip the line. Flag the inverse in one line where it occurs: a doctrine cited by a bead it was not bound to means its paths: is too narrow (or absent).
Append "6.5" to completed-steps, and carry the callouts + table into Step 10's commit body.
Step 7 — Draft beads (includes deferred-fixes AND parked-questions)
For each bead, deferred-fix, and parked-question candidate, compose the bead body in memory (a record with frontmatter fields + body sections — no file writes yet). Persistence happens in Step 9 and is tracker-aware:
bead-tracker: tbd→ ephemeral tempfile →tbd create --file→ unlink. Working tree untouched.bead-tracker: none→ markdown file atdocs/tasks/ongoing/<bead-slug>/bead.md.
Step 7 produces only in-memory records, the dedup decisions, and (next) the DAG over them.
Bead ID (interim): synth-<feature>-<YYYY-MM-DD>-<HHMM>-<NN> where <HHMM> is the skill-invocation time. The time suffix dedupes parallel-session runs on the same feature. If tracker is tbd, this ID is replaced by the tbd-assigned ID in Step 9; if tracker is none, this ID is final.
Cross-repo enum (F12): in-repo | cross-repo | mixed. mixed beads MUST include a ## Cross-repo dependency section in the body naming the sibling repo + the contract the in-repo work depends on.
Auto-fill <repo> placeholder (G11): use git remote get-url origin if a remote exists; otherwise pwd.
In-memory bead record (actionable beads — same shape regardless of tracker, only the destination differs):
---
id: synth-<feature>-2026-05-10-1742-01
title: <Imperative, scoped — e.g., "Extract symmetric-token helper from bootstrap-tunnel.sh + render-claw-config.ts">
type: devx-human | devx-agent | bug | drift | feature | optimisation
status: open
effort: XS | S | M | L
blocked-by: []
epic: <feature>
originating-spec: docs/tasks/completed/<feature>/<feature>-spec.md
originating-session: <YYYY-MM-DD>
cross-repo: in-repo | cross-repo | mixed
---
# <Title>
## Why now (session signal)
<One sentence: what surfaced this in the originating session.>
## Acceptance criterion
<Binary, verifiable. Include file paths and line numbers when known.>
## State-transfer prompt
> Paste the block below into a fresh Claude Code session along with the repo root.
>
> ---
> Working in <auto-filled repo URL or pwd>. Your task: <restate the acceptance criterion>.
>
> Relevant files:
> - <path:line> — <what it does, why it matters>
> - <path:line> — ...
>
> Relevant prior commits:
> - <SHA> — <one-line description>
>
> Constraints — do NOT modify:
> - <public surface / contract you must preserve>
>
> Verification commands:
> - <exact command>
> - <exact command>
> ---
## Cross-repo dependency <!-- only for cross-repo: mixed -->
<sibling repo + contract this bead depends on>
## Dependencies
- blocked-by: [<bead-id>, ...]
## Notes
<anything else useful — but resist sprawl>
Parked-question record (filed alongside beads, but with no acceptance criterion):
---
id: synth-<feature>-2026-05-10-1742-NN
title: <Question phrased as a question, scoped>
type: open-question
status: parked
originating-spec: docs/tasks/completed/<feature>/<feature>-spec.md
originating-session: <YYYY-MM-DD>
---
# <Title>
## The question
<Restated in full — what's ambiguous, what decision is owed, by whom.>
## Why parked
<Why this isn't actionable yet — no doctrine claim is at stake, no caller is currently confused, the answer depends on a future signal, etc.>
## When to revisit
<Concrete trigger — "next feature that touches X", "if Y starts happening", or "every quarterly review". Do not write "later".>
Parked-questions do not get a State-transfer prompt — they aren't work yet. They do not participate in the DAG (Step 8 skips them).
Dedup gate (mandatory, multi-source): before adding a record to the new-beads list, check:
- Existing tracker beads (tbd list, or
docs/tasks/ongoing/<*>/bead.mdif tracker isnone) - Past synthesis inventories (
docs/synthesis-index.mdif present)
If a similar bead exists, mark this candidate as an update-existing (target ID + a ## Update from session <YYYY-MM-DD> block to append) rather than a new-bead. Step 9 applies updates via the tracker's edit path (tbd) or by editing the markdown file in place (none). Near-misses (different wording, same intent) are flagged to the user for confirmation, not silently merged.
At the end of Step 7, the in-memory state is:
new-beads: [<record>, ...]— actionable bead candidates (including deferred-fixes) that will be created in Step 9parked-questions: [<record>, ...]— parked-question candidates that will be created in Step 9 (alongside beads, but skip the DAG)update-existing: [{target-id, append-block}, ...]— candidates that will append to an existing record in Step 9
Append 7 to completed-steps.
Step 8 — Build the DAG
Algorithm (G5):
Pairwise scan (skip parked-questions — they have no acceptance criterion and can't block other work). For each ordered pair
(A, B)of actionable beads, B isblocked-by: [A]iff B's acceptance criterion references files, symbols, exports, or commands that A's acceptance criterion creates, exports, or modifies. Record a one-line reasoning trace per edge.Cycle detection (F11) via Kahn's algorithm. Compute in-degree for each actionable bead; repeatedly remove zero-in-degree beads. If any bead remains after the pass, a cycle exists.
On cycle: REFUSE the bead set. Print the cycle (e.g.
A → B → C → A) and ask the user to break it (drop or split one bead). Do not silently emit a cyclic DAG.On success: encode
blocked-by:in each actionable bead's frontmatter. Append8tocompleted-steps.
Because every bead carries the epic:<feature> label (Step 7), the persisted graph — planned beads from /substrate:graph-spec plus these session-discovered ones — is inspectable as one card via bash docs/scripts/bead-graph.sh --epic <feature> (waves) or --format mermaid.
Topological layers, conceptually:
[Drift fixes — already committed in step 4]
↓
[Foundation beads — extracts, helpers] (e.g., symmetric-token lib)
↓
[Consumer beads — refactors depending on foundation]
↓
[Feature beads — net-new behaviour]
↓
[Optimisation beads — only after features land]
[Parked-questions — outside the DAG, no blockers, no work, status: parked]
Step 9 — Persist beads + parked-questions to tracker
This is the only step that performs bead I/O. Step 7 produced in-memory records; Step 8 wired blocked-by: across actionable beads. Persistence shape depends on the tracker.
Branch A — bead-tracker: none (markdown is canonical):
- For each record in
new-beads∪parked-questions: writedocs/tasks/ongoing/<bead-slug>/bead.md. The interimsynth-...ID from Step 7 is final. - For each record in
update-existing:Editthe target file in place to append the## Update from session <YYYY-MM-DD>block at end-of-file (or before any pre-existing## Notessection). - No tracker calls. No ID propagation pass (interim IDs were final).
Branch B — bead-tracker: tbd (tbd is canonical, this step is mandatory):
- Preview inline. Print each
new-beadsrecord to the chat as a fenced block — full body, in DAG order. Then print eachparked-questionsrecord (no DAG ordering needed). Then print eachupdate-existingrecord as<target-id>: + <append-block-preview>. - Ask:
Create N beads + K parked-questions + apply M updates via tbd now? (Y / n / select)— default isY(binary approval gate; no default-escape suffix).y(default): create / update all.select(P1): enters a per-recordy / n / skiploop — actionable beads in DAG order, then parked-questions, then updates. Skipped records are dropped (not persisted as scratch — there is no scratch path undertbd).n: skip persistence entirely. Warn the user: "Bead bodies were composed but not persisted. Because this repo is configured withbead-tracker: tbd, nothing is on disk. Re-run this skill or copy the previewed bodies above to recover."
- Create new beads. For each approved
new-beadsrecord, in DAG order (soblocked-by:references resolve to already-assigned IDs), then eachparked-questionsrecord:- Render the record body to a tempfile:
tmp=$(mktemp -t synth-bead-XXXXXX)then write the markdown body into$tmp. - Invoke
tbd create --type <type> -l "epic:<feature>" --file "$tmp" "<title>"— substitutenpx --no-install get-tbdfortbdif no global binary is onPATH. Theepic:<feature>label is the canonical epic identity (same one/substrate:architect-spec→/substrate:graph-specstamped on the planned beads), so session-discovered follow-up work groups under the same epic card. If an epic bead for this feature already exists —tbd list --type epic --label "epic:<feature>"returns one — also pass--parent <epic-id>so the bead nests as a subtask. For parked-questions, the type isopen-questionand the create command should setstatus: parked(via--statusflag if supported, else via frontmatter the tbd implementation reads). - Capture the assigned tbd ID from stdout. Record it in
{interim-id → tbd-id}. unlink "$tmp"— unconditional cleanup. The skill must not leave tempfiles behind even on partial failure (usetrapor equivalent).
- Render the record body to a tempfile:
- Apply updates. For each approved
update-existingrecord: invoke the tbd update path (tbd edit <id>ortbd append <id> --file <tmp>depending on tbd version) with the append-block. Same tempfile + unlink discipline. If tbd has no native append, fall back totbd show <id> > $tmp && cat append-block >> $tmp && tbd update <id> --file $tmpand unlink after. - Single-pass ID propagation (C7): with the full
{interim-id → tbd-id}map built, rewrite everyblocked-by:reference inside still-in-memory records before composing the synthesis-complete commit body in Step 10. Do this in one pass before exiting Step 9.
On failure of any tracker invocation: stop the batch, print the failing record's interim ID + the previewed body so the user can recover it manually, leave the tempfile in place (don't unlink on error), and exit with status: in-progress in the state file so a re-run can resume.
Aggregate appends (G10, P5):
- Append a one-line summary of each created bead (id, title, type, effort, blocked-by, cross-repo) to
docs/synthesis-index.md. This is the cross-session dedup ledger — append-only, not a per-feature artifact. - Append any cross-repo follow-ups identified during Step 3 to
docs/cross-repo-followups.md. Same append-only ledger pattern for the cross-repo decision-maker.
Both files are created with a header on first write.
Append 9 to completed-steps.
Step 10 — Handoff + synthesis-complete commit
Print this summary to the user:
✔ Session synthesis complete.
Feature: <feature>
Context: full | compacted
Doctrine fixes: <N> commits (cap hit: <yes/no>, demoted: <M>)
Doctrines authored: <N> {docs/doctrine/<id>-doctrine.md + manifest entry, each its own commit} (cap hit: <yes/no>, demoted: <M>)
Doctrine reconciled: in-epic by the executor's Doctrine Reconciliation phase (ratify-only) — not queued here
Beads drafted: <N> {in tbd | as files at docs/tasks/ongoing/<bead-slug>/bead.md} (tracker-dependent; incl. any non-ratify-only doctrine follow-ups as type=task)
Parked questions: <N> {in tbd as type=open-question status=parked | as files at docs/tasks/ongoing/<slug>/bead.md status=parked}
Doctrine bound vs cited: <N> bound doctrines · <K> flagged bound-but-never-cited (report-only — see commit body)
Pareto cut (top by leverage):
- <bead-id-or-title>
- <bead-id-or-title>
- <bead-id-or-title>
Session narrative:
<§1 narrative — two paragraphs max>
Audit trail: git log <base>..HEAD — every artifact is queryable from git + the tracker. No per-feature .md report file is written.
Next:
- Review the <N> authored draft doctrines (Status: Draft) — they're filled from session context; vet the rules, then promote to Binding.
- Pick a bead off the top of the DAG when you're ready to keep building (any non-ratify-only doctrine follow-ups are in there as actionable beads).
- git push when you're ready — this will push: the spec-execute commit, the <N> doctrine-fix commits, the <N> authored-doctrine commits, the post-execution-notes commit, and this synthesis-complete commit, together as one batch.
Then commit the state file with the narrative + Pareto cut as the body:
# Update state file: set status: complete, fill `completed` timestamp.
# narrative-commit will be backfilled by amend after this commit lands its SHA.
# Use the project's commit convention (inspect `git log -10` for trailers).
git add .substrate/synthesis-state.json docs/synthesis-index.md docs/cross-repo-followups.md
git commit -m "chore(<feature>): synthesis complete <YYYY-MM-DD>
§1 Session narrative
<two-paragraph narrative inline>
§7 Pareto cut — top items by leverage
- <bead-id-or-title> — <why high-leverage>
- <bead-id-or-title> — <why high-leverage>
- <bead-id-or-title> — <why high-leverage>
§6.5 Doctrine usage — bound vs cited
<any ⚠ bound-but-never-cited audit lines first, then the bound/cited/epic-level table>
Doctrine fixes this session: <SHA-list>
Doctrines authored this session: <id + path + SHA, one per line>
Beads created: <tbd-id-list or markdown-path-list>
Parked questions: <tbd-id-list or markdown-path-list>
"
After the commit lands, backfill .substrate/synthesis-state.json[<feature>].narrative-commit with the new SHA via git commit --amend --no-edit (or, to avoid amending, append a second commit chore: backfill narrative-commit pointer — pick whichever matches the project's amend posture; if recent git log shows the project doesn't amend published commits, use the second-commit path).
Set .substrate/synthesis-state.json[<feature>].status to complete and .substrate/synthesis-state.json[<feature>].completed to the current ISO8601 timestamp. Append 10 to completed-steps. Exit.
Constraints
- MUST NOT mix synthesis commits with the spec-execute commit. Each immediate fix is its own commit. The spec-execute commit must remain one revertable unit.
- MUST NOT create beads that duplicate existing beads. Run the dedup gate against both the tracker AND
docs/synthesis-index.mdunconditionally. - MUST NOT re-apply doctrine fixes that already shipped earlier in this session — apply the already-actioned filter (Step 2) before categorizing.
- MUST treat the bead-tracker as canonical when configured. If
bead-tracker: tbd, persistence for beads (Step 9) and parked-questions (Step 9) uses ephemeral tempfiles under/tmp(or$TMPDIR); Step 9 is mandatory with defaultY. Undertbd, the working tree MUST NOT receive any markdown file underdocs/tasks/ongoing/**— neither bead bodies nor parked-question bodies. Ifbead-tracker: none, markdown underdocs/tasks/ongoing/**is canonical. Never present tbd-persistence as an optional mirror in a tbd-configured repo — that inverts the source-of-truth and pushes the user toward dropping work on the floor. - MUST NOT queue doctrine amendments. There is no
type: doctrine-amendmentbead, nostatus: queueddead-letter, nodocs/tasks/ongoing/doctrine-updates/file. Ratify-only doctrine change lands in-epic via the executor's terminal Doctrine Reconciliation phase; non-ratify-only doctrine follow-ups become ordinary actionabletype: taskbeads (Step 9). Do not reintroduce the queue under any tracker. - MUST NOT write a per-feature synthesis report (no
docs/tasks/completed/<feature>/synthesis-*.md). The §1 narrative + §7 Pareto cut live in the synthesis-complete commit body. The two append-only aggregate ledgers —docs/synthesis-index.mdanddocs/cross-repo-followups.md— are the only.mdwrites outsidedocs/tasks/ongoing/<bead-slug>/bead.mdunder tracker=none, and they remain markdown regardless of tracker. - MUST persist resumability state in
.substrate/synthesis-state.json. Create the file with{}if missing. Append step numbers tocompleted-steps:after each step lands. On re-run withstatus: in-progress, RESUME — don't restart. - MUST carry the §1 narrative + §7 Pareto cut in the final synthesis-complete commit body — git is the audit log. Backfill
.substrate/synthesis-state.json[<feature>].narrative-commitwith that commit's SHA after it lands. - MUST unlink tempfiles after each successful
tbd create/tbd update(usetrapor equivalent). Leaving stray markdown bodies in/tmpis fine; leaving them in the working tree is the bug this design exists to prevent. On tracker-invocation failure, leave the
…(truncated)