- I searched existing issues and this has not been proposed before
Related, narrower: #2368 (DAG/wave execution inside SDD), #2245 (parallel dispatch checks), #2309 (DAG closure in plan self-review), #429 (Agent Teams). Those add scheduling to an ordinary task list. This issue covers what it took to make parallel build converge, and specifies it as a first-class alternate path.
Who's filing this request? (required)
Claude (Opus 5.5, Claude Code), filing for Jesse Vincent, Superpowers' primary author. Jesse's instructions: "open an issue … describing everything you built and how you would add this as an alternate plan and build implementation for superpowers so that every superpowers user could have this … think about how we would do it in a harness that doesn't have first-class workflows", then "update the issue with everything that Claude would need to know about how to build this. It's going to be another Claude … include versions of all necessary scripts … not describe anything about what [the project] is. You can speak in Claudese." So this issue is a build brief for another Claude instance, in dense form.
Human-authored summary (required, under 200 words)
Jesse hasn't written one. In its place, his request verbatim: "Could you open an issue against obra/superpowers Describing everything you built and how you would add this as an alternate plan and build implementation for superpowers so that every superpowers user could have this? Also think about how we would do it in a harness that doesn't have first-class workflows."
What problem does this solve?
writing-plans + SDD = one ordered task list, one implementer at a time. That's correct by default, but too slow for big builds. The real session: spec → ~26 work packages → ~100k lines of code and tests, built overnight by fanned-out agents.
Naive fan-out failed: a master plan, then N agents each writing a package plan in parallel. A cross-package critic then found:
- 24 interface mismatches. Each package invented its own wire types and fields.
- 13 shared-file overlaps. Packages all edited
lib.rsmod lines, manifests, lockfiles, and the route tree. - 7 dependencies on same-wave work.
- ~200 contract change requests and ~72 "open questions", mostly duplicates.
Root cause: too little of the shared surface was frozen. A scheduler alone (#2368) can't fix disagreeing packages.
Proposed solution
Overview
brainstorming → spec (unchanged)
→ lead writes 00-master.md: decomposition, DAG, constraints, shared-file policy [single mind, not fanned out]
→ parallel-plan-converge: registry → verify → package plans → review → CCRs → triage → critic loop until dry
→ human answers the short "Open questions" list (or authorizes recommendations)
→ parallel-build: foundations package alone first, then an audit of foundation contracts
→ DAG fan-out: worktree TDD → competing reviewers ×2 rounds → merge train → gate
→ stop at approval-gated actions (infra apply, push, external services)The scripts are posted as comments below: parallel-plan-converge.workflow.js and parallel-build.workflow.js. Both are generalized, parameterized versions of the scripts that ran the real build, with no project specifics.
Deliverables
skills/writing-parallel-plans/SKILL.md: when to choose this path, the lead's job (master plan), how to run convergence, how to handle the human question list.master-template.md,registry-format.md,package-template.md,decisions-log.md: all spec'd below.parallel-plan-converge.workflow.js(comment 1) for harnesses with the Claude Code Workflow tool.manual-converge.md: the same steps as plain subagent dispatches, for other harnesses (see "No-workflow harnesses").
skills/executing-parallel-plans/SKILL.md: preconditions, the foundations-first rule, the audit step, stopping rules, how to report to the human.- Prompt files:
implementer-prompt.md,reviewer-spec-prompt.md,reviewer-code-prompt.md,fixer-prompt.md,integrator-prompt.md,gate-prompt.md,foundations-audit-prompt.md. The first six can be pulled from the script; the audit prompt is below. parallel-build.workflow.js(comment 2).manual-scheduler.md+plan-state.schema.jsonfor no-workflow harnesses.
- Entry points:
brainstorming/writing-plansscope check: when the spec implies more than ~8 independent packages, or the human asks for fan-out, offerwriting-parallel-plans.- SDD: "plans with
00-master.md+01-contracts.md→executing-parallel-plans".
- Vocabulary: reuse #2368's task metadata (
Depends on, write scope) where it overlaps. Don't invent a second dialect.
Artifacts (formats)
00-master.md is written by the lead alone. Decomposition needs one coherent mind.
- §1 Global constraints: stack, datastore, test policy (real services, no mocks in integration/e2e, pristine output), style rules, commit trailer.
- §2 Layout + shared-file policy (this was the biggest single fix):
- A foundations package (wave 1) pre-creates every module file, every
mod/import line, every dependency in every manifest, the whole route registry with placeholders, and the shared test fixtures, all straight from the registry. No later package edits these files. - Lockfiles and generated artifacts (lockfiles, OpenAPI snapshot, generated client types) belong to the integrator. Package agents never commit them.
- Non-leaf modules hold only declarations and re-exports, so leaf files are ownable.
- Each worktree gets its own service instance (database, etc.), with names derived from the worktree path. Add a reset recipe: agents edit unshipped migrations in place, and dev databases then reject the checksum.
- A package needing anything outside its Owns stops and reports. It never reaches across.
- A foundations package (wave 1) pre-creates every module file, every
- §3 Frozen core contracts: the types every package touches. The registry extends and supersedes this section.
- §4 Package table + DAG: name, deps, scope, rough size. Split the big packages: the largest package sets the wall clock.
- §5 Orchestration + stopping rules: never push; no infrastructure apply, external publish or other irreversible action without explicit human approval; stop on a red gate that can't be fixed.
- §6 Cross-cutting acceptance scenarios: the e2e package builds these, and earlier packages design toward them.
- §7 Review focus: likely failure modes no single package owns.
01-contracts.md + 01-contracts/<area>.md is the contract registry:
- Each part is written by its own author, in parallel. Good areas: HTTP-by-domain groups; cross-module code API + schema + events; layout (modules, deps, routes, fixtures, CLI syntax/output/exit codes).
- Every entry names its producer package and its consumer packages. HTTP rows give method, path, request and response types (with field types), and error codes. Rust/TS rows give the exact path and signature.
- Authority: the registry wins over package plans and never contradicts the spec.
- Each part ends with a "Resolved conflicts" list. The index has an append-only "Registry fixes" log naming the packages each fix affects.
packages/<name>.md follows the template embedded in the converge script:
- Spec sections; Depends on; Owns (exact paths, disjoint within a wave); reserved numbering.
- Scope in/out; Interfaces (Consumes/Produces using exact registry names).
- Acceptance tests with concrete assertions; TDD steps; the gate command.
- "Decisions made here" plus contract change requests. Package writers never edit the registry: CCRs are applied centrally.
02-decisions.md is the decisions log:
- Append-only, with
merge=unionin.gitattributes, because every fix branch appends to it. - It has three sections: decided (builder-level), "Open questions for the human" (each with a recommendation), and "Decisions made during execution".
- Mark each entry
[decided]or[pending human]. If the human authorizes recommendations ahead of time, adopted items stay marked pending, so they can be audited the next morning.
Execution rules that mattered (all encoded in the build script)
- Foundations first, alone. Then run an adversarial foundation-contracts audit (prompt below) before fanning out. In the real session it found a privilege escalation that two per-package reviews had missed: an item-scoped credential could take workspace-wide admin actions. It also found a deadlock between lock orders and roles that weren't enforced in the DB.
- DAG scheduling, not wave barriers. A package starts when its specific deps have merged. With wave barriers, finished packages sat idle for about 2.5h waiting on the slowest.
- Per package: an isolated worktree + its own services, then a TDD implementer, then two competing adversarial reviewers (
/parpattern; lenses: spec+contracts and correctness+security+style), then a fixer.- Cap at 2 rounds. Round 1 fixes everything. Round 2 fixes only critical/major, and its moderates go to the gate. Rounds beyond 2 rarely paid off.
- Merge train: finished packages queue up, and one integrator at a time merges the whole queue in dependency order. It regenerates lockfiles and codegen per merge, then runs the full check once per batch, bisecting only when it's red. Full CI after every merge cost more than 1h on an 8-package wave.
- After merging, it releases per-worktree resources (stops containers). Otherwise dozens pile up.
- Gate: a completeness critic compares what was built against the plans and spec, and also judges leftover moderates. A gate-fix agent then closes the gaps test-first.
- Between batches, the lead reads results and runs targeted fix branches for anything a gate flagged as a design issue. Those fixes merge before any dependent packages start.
- CI must include the browser e2e suite, which starts and stops its own stack. Otherwise web gates quietly never run.
- Build cache: sccache shares third-party crates across worktrees. It does not share workspace crates (absolute paths differ) or test binaries. A shared
target/serializes on cargo's lock, so don't use one.
Foundation-contracts audit prompt (not in the scripts)
Read-only adversarial audit of the merged foundations package. Every later package builds on it, so contract mistakes multiply; false or inflated findings are worse than useless. Read the registry, master §1–§3, the foundations plan, and the spec's access/privacy section. Compare them against the code and the schema. Report, ranked critical/major/moderate, each with file:line + registry § + a one-line fix:
- contract drift (missing or reshaped types, signatures, variants, tables, columns);
- schema safety: append-only enforcement, row-level isolation on every tenant table, role grants actually enforced by the DB (not just by convention);
- access-logic edge cases: credential narrowing, delegated actors, humans-only rules, governance/admin actions under narrowed credentials;
- concurrency: ordering/gap-freedom claims actually tested under concurrent commits; lock-order deadlocks between triggers and app code;
- missing pre-created files or dependencies that later packages need.
No-workflow harnesses
The orchestration needs only: spawn a subagent with a prompt, run several at once, and collect structured results. Everything else is plain files.
plan-state.json(checked into the plan dir, written only by the orchestrator) survives compaction and resumption:{"integrationBranch":"wip/integration", "packages":{"pkg-a":{"deps":[],"status":"merged|queued|reviewing|implementing|pending|blocked", "branch":"pkg/pkg-a","worktree":"/abs/path","round":1,"blockers":[],"leftoverModerates":[]}}, "batches":[{"packages":["pkg-a"],"ciGreen":true}], "gate":{"gaps":[],"done":false}}- Manual scheduler loop (
manual-scheduler.md):- Read the state.
- For every pending package whose deps are all merged: create a worktree (
git worktree add -b pkg/<n> <path> <integrationBranch>if the harness has no isolation), then dispatch the implementer in the background with the path in its prompt. Mark it implementing. - When an implementer reports, dispatch both reviewers at once, then the fixer. Repeat up to the round cap, then mark it queued.
- When nothing is implementing or reviewing, or the queue holds ≥ N packages, dispatch the integrator for the whole queue. On green, mark them merged and go to step 2.
- When everything is merged or blocked, run the gate.
- Write the state after every transition.
- Harnesses with subagents but no worktree isolation (Codex, etc.): the orchestrator makes the worktrees. Each subagent writes a required JSON report to
<worktree>/.parallel-report.json, and the orchestrator reads that file instead of relying on the agent's final text. - Harnesses with no subagents at all: run serially over the same artifacts.
- Run the reviewers as separate passes with an explicit adversarial persona and a fresh re-read of the files.
- The registry and ownership rules still pay off, because they prevent drift across context resets.
- For real parallelism, drive separate sessions with
claude-session-driveror tmux.
Acceptance for the implementing Claude
- Plan-side pressure test (
writing-skillsstyle): given a multi-module spec, the agent writes the master plan itself, doesn't fan it out, and produces a registry where every entry names its producer and consumers, and package Owns lists don't overlap within a wave. - Build-side pressure tests:
- the executor runs foundations alone, then the audit, then fans out;
- it refuses to apply infrastructure without approval;
- an injected "need a file outside Owns" ends up in blockers, not as a cross-package edit.
- Manual-scheduler walkthrough: in a harness without the Workflow tool, a 4-package DAG runs through to the gate with
plan-state.jsonas the only memory. - Both scripts parse and run under the Claude Code Workflow tool: pure JS,
export const metaliteral first, noDate.now()/Math.random().
What alternatives did you consider?
- Scheduler over a task list (#2368): necessary but not sufficient. Plans drifted even with perfect scheduling.
- Hard wave barriers: simple, but the slowest package gates everyone. DAG + merge train is strictly better.
- Shared build dir: serializes builds on the build-tool lock.
- More review rounds: diminishing returns past 2. Gates and foundation audits caught what rounds 3+ didn't.
Is this appropriate for core Superpowers?
Yes, as an opt-in path. Nothing here is specific to a domain or stack. The registry, ownership policy, convergence critic, merge train and gates apply to any multi-module codebase large enough to parallelize. The manual scheduler keeps it working across harnesses.
Environment (required)
| Field | Value |
|---|---|
| Superpowers version | local checkout v7.0.1-309-g0f9581f |
| Harness (Claude Code, Cursor, etc.) | Claude Code (Workflow tool for orchestration) |
| Harness version | 2.1.308 |
| Your model + version | Claude Opus 5.5 (claude-opus-5-5) |
| All plugins installed | superpowers, superpowers-lab, superpowers-chrome, episodic-memory, elements-of-style, claude-session-driver, frontend-design, design, plugin-dev, others from claude-plugins-official |
Context
Process lessons for the skill text:
- The lead must read every gate result and turn design-level findings into targeted fix branches before dependents start. Don't let the runner auto-continue past them.
- Merge agents drift from the commit conventions unless the prompt spells them out.
- Integrators should resolve append-only-log conflicts by union;
.gitattributeshandles it. - Tell implementers where secrets live, and that worktrees inherit the parent
.env. They must never print or commit it. - Write the registry before the package plans. The real session did it after, and paid for two convergence rounds.
Activity
2parallel-build.workflow.js
Comment 2 of 2. This is the build half. Run it after convergence, once the human has answered the short question list. Run foundations alone first (packages: [{name: 'foundations', deps: []}]), then the foundation audit, then everything else.
export const meta = {
name: 'parallel-build',
description: 'Execute a converged parallel plan: per-package worktree TDD, competing reviewers (2 rounds max), DAG scheduling, merge-train batches with one CI each, gate critic',
whenToUse: 'After parallel-plan-converge reports no new critic issues. args: see ARGS below.',
phases: [
{ title: 'Implement', detail: 'each package starts when its deps are merged' },
{ title: 'Review', detail: 'spec/contract lens + correctness/security/style lens' },
{ title: 'Fix', detail: 'round 1 fixes all findings, round 2 critical/major only' },
{ title: 'Merge', detail: 'merge train: batch finished packages, one full check per batch' },
{ title: 'Gate', detail: 'completeness critic over everything built + leftover findings' },
],
}
// ARGS
// {
// root: '/abs/path/to/repo', // main checkout; must be on `integrationBranch`, clean
// integrationBranch: 'wip/integration',
// spec: 'docs/.../spec.md', // repo-relative
// planDir: 'docs/.../plans/<plan>', // contains 00-master.md, 01-contracts.md(+dir), 02-decisions.md, packages/<name>.md
// extraReading: ['design/UX-SPEC.md'], // optional, repo-relative, read by every agent
// pkgCheck: 'just check-pkg {name}', // package gate; {name} substituted
// fullCheck: 'just ci', // whole-repo gate run once per merge batch
// postMerge: 'cargo generate-lockfile; ...', // optional: regenerate lockfiles/codegen after each merge
// commitTrailer: 'Claude-Session: ...', // optional line every commit message must end with
// cleanup: 'docker ps --filter label=com.docker.compose.project.working_dir={worktree} -q | xargs -r docker stop', // optional; {worktree} substituted
// merged: ['pkg-a', ...], // packages already on integrationBranch
// packages: [{ name: 'pkg-b', deps: ['pkg-a'] }, ...], // in a topological order
// }
// Worktrees branch from the main checkout's HEAD at launch, so a package launched after its deps
// merged sees their code. Never push; never run outward-facing or irreversible actions.
const A = args
const R = A.root
const P = `${R}/${A.planDir}`
const TRAILER = A.commitTrailer ? ` Every commit message ends with the line "${A.commitTrailer}".` : ''
const READING = `Read first: spec ${R}/${A.spec}; master plan ${P}/00-master.md; contract registry ${P}/01-contracts.md and every file under ${P}/01-contracts/ if present; decisions log ${P}/02-decisions.md${(A.extraReading || []).map(x => `; ${R}/${x}`).join('')}.`
const sub = (s, k, v) => (s || '').split(`{${k}}`).join(v)
const FINDINGS = { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, severity: { type: 'string', enum: ['critical', 'major', 'moderate'] }, file: { type: 'string' }, evidence: { type: 'string' }, fix: { type: 'string' } }, required: ['title', 'severity', 'file', 'evidence', 'fix'] } } }, required: ['findings'] }
const IMPL = { type: 'object', properties: { branch: { type: 'string' }, worktree: { type: 'string' }, check_green: { type: 'boolean' }, summary: { type: 'string' }, blockers: { type: 'array', items: { type: 'string' } } }, required: ['branch', 'worktree', 'check_green', 'summary', 'blockers'] }
const MERGE = { type: 'object', properties: { merged: { type: 'array', items: { type: 'string' } }, ci_green: { type: 'boolean' }, summary: { type: 'string' }, problems: { type: 'array', items: { type: 'string' } } }, required: ['merged', 'ci_green', 'summary', 'problems'] }
const GATE = { type: 'object', properties: { gaps: { type: 'array', items: { type: 'object', properties: { requirement: { type: 'string' }, package: { type: 'string' }, evidence: { type: 'string' } }, required: ['requirement', 'package', 'evidence'] } } }, required: ['gaps'] }
const implementPrompt = (n) => `Implement work package **${n}** in your own git worktree. ${READING} Then read your package plan ${P}/packages/${n}.md and follow it.
- First: git switch -c pkg/${n}
- TDD: failing test → run, see it fail for the right reason → minimal implementation → green → commit.${TRAILER}
- Touch only the paths your plan's Owns lists (master shared-file policy). Never commit lockfiles. Need something outside Owns, or a contract change? Stop that part; list it in blockers.
- No mocks in integration/e2e tests. Test output pristine: expected errors are captured and asserted.
- Done = \`${sub(A.pkgCheck, 'name', n)}\` green. Fix root causes; never weaken tests or lints.
Return branch, absolute worktree path, check_green, summary, blockers.`
const LENS = {
spec: 'LENS spec+contracts: everything the package plan and spec require in scope, with exact registry names/signatures/endpoints/fields; access and privacy rules enforced and tested on every path touched; tests exercise real behavior (no mocked behavior, no tautologies).',
code: 'LENS correctness+security+style: bugs, races, isolation/tenancy leaks, injection, credential handling, panics/crashes outside tests, style-rule violations.',
}
const reviewPrompt = (n, c, lens) => `Adversarial reviewer. You compete with another reviewer: most legitimate significant findings wins; a false or inflated finding disqualifies you. Package ${n}; worktree ${c.worktree}; branch ${c.branch}; diff: git -C ${c.worktree} diff ${A.integrationBranch}...HEAD. ${READING} Plan: ${P}/packages/${n}.md.
${LENS[lens]}
You may run tests in the worktree. Do not edit. Report only significant findings, each with evidence (file:line, failing output, or spec quote) and a one-line fix.`
const fixPrompt = (n, c, f) => `Fix review findings for package ${n}. Work ONLY in existing worktree ${c.worktree}, branch ${c.branch} (cd there). ${READING} Plan: ${P}/packages/${n}.md.
Findings: ${JSON.stringify(f, null, 2)}
Verify each. Real → failing test first, then fix. Wrong → reject with reason. Stay within Owns.${TRAILER} Finish with \`${sub(A.pkgCheck, 'name', n)}\` green. Return the same shape; summary lists fixed vs rejected.`
const leftovers = []
const results = {}
async function buildPackage(p) {
const impl = await agent(implementPrompt(p.name), { label: `impl:${p.name}`, phase: 'Implement', isolation: 'worktree', schema: IMPL })
if (!impl) { results[p.name] = { ok: false, reason: 'implementer failed' }; return null }
let cur = impl
for (let round = 1; round <= 2; round++) {
const [a, b] = await parallel([
() => agent(reviewPrompt(p.name, cur, 'spec'), { label: `review-spec:${p.name}#${round}`, phase: 'Review', schema: FINDINGS }),
() => agent(reviewPrompt(p.name, cur, 'code'), { label: `review-code:${p.name}#${round}`, phase: 'Review', schema: FINDINGS }),
])
const all = [...(a ? a.findings : []), ...(b ? b.findings : [])]
const toFix = round === 1 ? all : all.filter(f => f.severity !== 'moderate')
if (round === 2) all.filter(f => f.severity === 'moderate').forEach(f => leftovers.push({ package: p.name, ...f }))
if (!toFix.length && cur.check_green) break
const fixed = await agent(fixPrompt(p.name, cur, toFix), { label: `fix:${p.name}#${round}`, phase: 'Fix', schema: IMPL })
if (fixed) cur = fixed
}
results[p.name] = { ok: cur.check_green, ...cur }
return cur
}
// Merge train: finished packages queue; one integrator at a time takes the whole queue,
// merges in dependency order, and runs the full check once for the batch.
const merged = new Set(A.merged || [])
const queue = []
const batches = []
let mergeChain = Promise.resolve()
function scheduleMerge() {
mergeChain = mergeChain.then(async () => {
if (!queue.length) return
const batch = queue.splice(0)
const order = A.packages.map(p => p.name).filter(n => batch.find(b => b.name === n))
const by = Object.fromEntries(batch.map(b => [b.name, b]))
const cleanup = A.cleanup ? `\nAfter all merges, release each merged package's per-worktree resources: ${order.map(n => '`' + sub(A.cleanup, 'worktree', by[n].worktree) + '`').join('; ')}.` : ''
const res = await agent(`Integration agent, merge-train batch. Main checkout ${R}, branch ${A.integrationBranch}; verify you are on it and the tree is clean (ignored files aside).
Merge in this order: ${order.map(n => by[n].branch).join(', ')}.
- After each merge: ${A.postMerge ? '`' + A.postMerge + '`, then ' : ''}commit. No full check per merge.
- After ALL merges: \`${A.fullCheck}\` once. Red → find the breaking merge (bisect within the batch), fix the root cause (with a test for behavior bugs), rerun until green.${cleanup}
- Never weaken tests or lints. Never commit secrets.${TRAILER} ${READING}
Return merged branches, ci_green, summary, problems.`, { label: `merge:${order.join('+')}`, phase: 'Merge', schema: MERGE })
batches.push({ order, res })
if (res && res.ci_green) order.forEach(n => merged.add(n))
else log(`batch ${order.join(', ')} not green; its dependents will not start`)
launchReady()
})
return mergeChain
}
const running = []
const started = new Set()
function launchReady() {
for (const p of A.packages) {
if (started.has(p.name) || merged.has(p.name)) continue
if (!p.deps.every(d => merged.has(d))) continue
started.add(p.name)
log(`start ${p.name}`)
running.push(buildPackage(p).then(r => { if (r) { queue.push({ name: p.name, ...r }); return scheduleMerge() } }))
}
}
launchReady()
// New packages launch from inside merges; keep awaiting until nothing new started.
let seen = 0
while (seen < running.length) {
const n = running.length
await Promise.all(running.slice(seen, n))
seen = n
await mergeChain
}
phase('Gate')
const built = A.packages.filter(p => merged.has(p.name)).map(p => p.name)
const notBuilt = A.packages.filter(p => !merged.has(p.name)).map(p => p.name)
if (notBuilt.length) log(`not merged: ${notBuilt.join(', ')}`)
const gate = await agent(`Gate critic for packages ${built.join(', ')}. In ${R} on ${A.integrationBranch}, compare what was built against each package plan in ${P}/packages/ and the spec sections they claim. ${READING}
Also judge these leftover moderate review findings; keep only real ones: ${JSON.stringify(leftovers)}
Report only real gaps with evidence: planned acceptance test missing/skipped; registry contract not implemented as specified; access/privacy rule not enforced; confirmed leftover finding. Run tests as needed. Read-only.`, { label: 'gate', phase: 'Gate', schema: GATE })
let gateFix = null
if (gate && gate.gaps.length) {
gateFix = await agent(`Close these gaps in ${R} on ${A.integrationBranch}: failing test first, then fix; then \`${A.fullCheck}\` until green.${TRAILER} ${READING}
Gaps: ${JSON.stringify(gate.gaps, null, 2)}
Return merged (empty), ci_green, summary, problems.`, { label: 'gate-fix', phase: 'Gate', schema: MERGE })
}
return {
packages: A.packages.map(p => ({ name: p.name, merged: merged.has(p.name), ok: results[p.name] ? results[p.name].ok : null, blockers: results[p.name] ? results[p.name].blockers : null })),
batches: batches.map(b => ({ order: b.order, ci_green: b.res ? b.res.ci_green : false, problems: b.res ? b.res.problems : ['integrator failed'] })),
leftover_moderates: leftovers.length,
gate_gaps: gate ? gate.gaps.length : null,
gate_fix: gateFix,
}
parallel-plan-converge.workflow.jsComment 1 of 2. This is the planning half. Run it after the lead writes
00-master.md. It produces the registry, the package plans, and the decisions log, then loops a cross-package critic until it stops finding new issues. It never commits.