Imported from ahmcgee/roadmap-orchestration (
plugins/roadmap-orchestrator/skills/orchestrate/SKILL.md). Install upstream withnpx skills add ahmcgee/roadmap-orchestration --skill orchestrate. Copyright stays with the author.
Roadmap Orchestrator
One session, one arc: from an unstructured roadmap to a tested, reviewed, integrated branch. You — the frontier model reading this — are the architect. You plan, you gate, you decide. You never write implementation code.
Two generic scripts in this skill's directory do all coordination for zero model tokens:
conductor.mjs (the default dispatch path) loops the arc's waves in one run and triages
each wave boundary through a tiered ladder, so you wake once per run rather than once per
boundary; harness.mjs is the single-wave executor it dispatches, and the fallback you can
launch per-wave yourself. Neither is rewritten per run.
This file is what to achieve. Data shapes, config knobs, and the harness/conductor internals
are in reference.md — read it before Phase 0. Design rationale, where you want it, is in
RATIONALE.md; you don't need it to operate.
Invariants — never break these; everything else is judgment
- Every delegation names its model explicitly. The scripts already do. Any agent you spawn must too — and never a typed agent (Explore, Plan, …) without a pinned model: they inherit your model and silently bill recon sweeps at frontier prices.
- Claude decides, Codex drafts and executes, Haiku only couriers. You, and every
fableagent, produce plans, contracts, specs, directives, verdicts, reports — never code, never bulk text. The Codex CLI writes ALL implementation and fixes, plans each unit's own work, runs the spec's verification lanes, reads the diff into a pre-gate review digest and — since 0.14.0 — drafts at the boundary too (the wave-tail explorer, health assessor, flake band and design reconciler, each writing its own report); every one of those is steered by a Haiku agent. Claude keeps every surface that can REJECT work: the plan-check, both exit gates, the escalation ladder, the consults, the merge and its suite gate, and the boundary TRIAGE that rules on what those roles found. Opus judges; Sonnet gates low-risk units, extracts and compresses; Haiku runs closed command lists and writes down what the script already composed — including WHERE each command runs: since 0.14.0 every composed command carries its owncd '<dir>' && ( … )guard, so a courier that skips the instruction fails that command's exit code rather than answering from whatever checkout it happened to start in. A codex outage is a hard stop to surface to the user, never a licence for a Claude agent to implement in its place. - All loops are bounded. Fix rounds, gate rounds, consults, and the conductor's wave loop are capped in config. When a bound is hit, quarantine and move on — quarantine is a normal outcome that feeds redesign, not a failure to retry around.
- Contracts freeze when execution starts. Amendments happen only through a gate or consult directive, are recorded in state, and are re-examined at your session integration review.
- Integration is serial, and
mainis untouched until the user confirms at session end. The session's product is one integration branch. - Don't rewrite the scripts per run. Parameterize them through the plan pack and config. If
a unit doesn't fit its stages, give it a different
kindand handle it yourself between waves. A deliberate script change is not done until the whole three-tier eval ladder passes (evals/parse.sh→evals/unit/run.sh→ the paid fixtures; seeevals/README.md), and you never edit either script while an arc is in flight. - Workflows take no mid-run input. Every ambiguity you leave unresolved in Phase 0 becomes a
quarantine later — ask the user now or decide now. A boundary agent that needs an answer
early-returns
needs-user(question in itsnotes) rather than pausing for it. - Feedback accumulates; it never steers. Explorer findings, health findings, and user notes
land in
.roadmap/feedback/(in issue mode a user-reported bug is aroadmap:bugissue instead — same non-interrupt rule: read at the next boundary, or the next Phase 0 between sessions) and wait for the next judgment boundary. Nothing there may interrupt, reroute, or message an in-flight unit. This is about timing, not identity: the conductor's tiers triage on your behalf, but only at the same wave tail you would have woken at. Root-only, always (a tier early-returns instead): contract amendments, contingent replans, needs-user calls. Technical debt follows the pinned scope envelope: in-scope correctness is fixed in-unit and never banks through an approve; anything outside the unit's declared scope BANKS by default with a stated closed-setbankReason— a directive that widens a diff beyond its scope costs more than the imperfection it removes (this inversion is deliberate; it is what killed the review spiral). While the arc still has planned work to run, even minor debt folds into the next wave as fix-work — but debt never creates a wave, so leftover debt at arc end is durable (the living.roadmap/debt.md, or consolidated per-unitroadmap:debtissues in issue mode) and carries to the next session's Phase 0.
Phase 0 — Plan (interactive; the highest-leverage act in the system)
Inputs: the roadmap in whatever form it arrives, the user's cut line ("build up to …"), and the repo you're standing in.
First, check for an existing .roadmap/. A top-level state.json means an arc is in flight
— resume it or ask the user; never plan over it (its stale integrationTip would silently fork
new worktrees from a dead base). archive/ and the living documents (constraints.md,
debt.md, notes) are prior knowledge: constraints carry forward, unresolved debt is candidate
scope you weigh against the cut line, archived contracts may seed new ones but are not binding.
A fresh arc starts only from a closed-out .roadmap/.
Resolve the tracking mode. Probe once for a usable GitHub remote and gh auth (gh auth status,
git remote -v, gh repo view --json nameWithOwner). Present → issue mode: set
plan.tracking: "issues" and plan.repoSlug, and work tracking lives in GitHub issues (the full
label/kind/state scheme, markers, and sync map are in reference.md → "GitHub issue tracking" — don't
restate them). Absent → set plan.tracking: "files" and everything below behaves exactly as the
filesystem design always has. Either way the scheduler runs on state.json — issues are a
projection Haiku maintains, never something the scripts read. In issue mode, read open roadmap:debt,
status:proposed unit issues, and open roadmap:bug issues as candidate scope (a user proposal or
bug is roadmap input, not a ready spec) in place of reading debt.md. Adjudicate a proposal or bug
the same way — adopt / split / fold / defer / decline-with-reason — and resolve its source issue:
adopt (1:1) promotes the source issue in place (flip its status to status:pending, write the
spec + <!-- roadmap:unit id=<id> --> marker, attach milestone + wave/risk); split (1:N)
opens N child unit issues and closes the parent with a comment linking them; fold/defer/decline
open no new unit (fold into an existing spec, leave open, or close not-planned with a reason). Full
mechanic in reference.md → "backlog / proposals". This is Phase-0 scope-setting, distinct from the
mid-arc debt sweep — adopting a bug here legitimately plans a wave; the "debt never creates a wave"
brake is a tier-2 guarantee and is unaffected.
Codex preflight (REQUIRED — refuse to dispatch without it). The implementer for every unit
is the codex CLI, launched by cheap steering agents inside unit worktrees; there is no Claude
implementation lane. Probe once: command -v codex && codex --version && codex login status,
then — because a valid credential proves nothing about the SERVICE (2026-09-03: the ChatGPT Codex
backend 404'd every run while login status still said "Logged in") — one real bounded run:
timeout 120 codex exec --skip-git-repo-check 'Reply with exactly the word pong'. The pass test
is its exit code, not its wording. (Prefix CODEX_HOME=<home> on all of them if the
environment uses a non-default home — check $CODEX_HOME.)
All three green → record plan.codex: { home: <the CODEX_HOME path or null> } and continue. Not
logged in or binary absent → stop before dispatch and tell the user exactly what to run:
codex login (browser) or codex login --device-auth (headless), or install the CLI. Smoke
non-zero with the first two green → stop before dispatch too, but say the opposite thing: the
CLI and the credential are fine, the Codex backend is down, and no login will help — wait it out.
Auth is a human act — never attempt the login yourself. Mid-arc, the harness re-probes (all three
commands) each wave, and a backend that dies mid-wave trips a breaker on ≥2 consecutive codex
failures across different units or roles with the same HTTP status; either way it
early-returns codex-unavailable / codex-usage-limit with the state intact; both are
resumable pauses (re-auth, or wait for the limit window or the outage, then relaunch), never
failures to route around by re-implementing with Claude. The same shape covers the host and the platform:
env-pids-exhausted / env-no-reaper (the pre-dispatch host preflight), env-verify-blocked
(two units' verification tooling could not run in one wave — a host fact, not two unit defects)
and platform-outage (required agent results stopped arriving) park the wave the same way — see
state.halt.
Delegate the bulk reading, keep the thinking: a Sonnet agent normalizes the roadmap into candidate items, stated dependencies, and ambiguities; Opus agents (models pinned) produce a codebase brief — module map, build/test commands, conventions, test-suite strength, hot files. Read their outputs, then decide:
- Decompose into units that are independently verifiable — each builds, its tests pass, and "done" is a crisp, runnable check. Size generously: a unit is as large as you can specify with no open questions left (the grilling bar below). Every unit pays a fixed cost — plan, plan-check, verify, review, gate, merge, plus the executor's cold start — no matter how little work it carries, so fragmenting multiplies machinery rather than buying safety. The bounds are structural, not durational: each unit needs its own branch/gate/merge slot and a diff one reviewer could hold in their head. Stop growing a unit where recovery cost overtakes the saving — a bigger diff makes gate rejection more expensive and the pinned fix envelope less precise. Cut along interfaces, not features. Minimize file overlap between units that could run concurrently. If "done" isn't checkable, the unit is too big or under-specified.
- Freeze contracts — the interfaces shared between units (types, signatures, schemas,
conventions) — into
.roadmap/contracts/before anything builds. This is your main weapon against cross-unit incompatibility; the merge gate only catches what it can't prevent. Alongside them, freeze one standing conventions contract (contracts/conventions.md, pointed to byplan.conventions): the shared utilities every unit must reuse rather than reinvent, plus the naming, error-handling, and recurring-pattern conventions every unit must follow. It binds all units, and the harness threads it into every implement/review/gate. Its reach is bounded, though: it binds work against the surface that exists now, so it can't stop two concurrent units from independently adding the same new helper — that case is caught reactively by the between-wave health check. Where the repo carries a numbered artifact sequence (schema migrations, ordered codegen steps — anything whose filenames start with an allocated number), the conventions contract must pre-allocate explicit numbers per unit at Phase 0 and say so per spec; "next free number at dispatch" is a collision generator under parallelism (arc-observed: two duplicate-prefix pairs in one arc, one of which silently erased a CHECK constraint at merge). Setplan.prefixUniqueGlobs(e.g.["migrations/*"]) so the merge path refuses a duplicate prefix mechanically instead of trusting the allocation held. The check compares the pre-merge tip against the merged tree and refuses only a duplicate the merge introduces — a repo whose history already holds grandfathered duplicate pairs (sealed by its own migration-number seal) is safe to guard. Still grep the tree at Phase 0 and record any pre-existing duplicates in the conventions contract, so no unit "fixes" them by renumbering. - Set
plan.scopeAllowfor the repo's evidence/test conventions (e.g.["docs/evidence/**", "**/test/**", "**/*.test.*"]) — and record the key explicitly even when the answer is[], so the next reader can tell "this repo has no by-convention files" from "nobody decided" (arc-observed: a 19-wave arc where Phase 0 simply never set it, and every unit's own test files read as growth). Every unit's pinned scope envelope is stated to the implementer, and diff files beyond it raisescope-growthfor the exit gate to adjudicate. Files matchingscopeAlloware in scope by convention and never counted as growth — otherwise a unit's own evidence screenshots, transcripts and sibling test files raise the signal on every wave (arc-observed: 7scope-growthdegradations in one wave, nearly all noise) and drown the real one (a unit reaching into another area's source). - Cross-check contracts against code before freezing. Where a frozen surface already exists
in code (skip only if every frozen surface is greenfield), have a Haiku agent (Sonnet where
signatures are subtle) list the surfaces each drafted contract freezes — endpoints, CLI verbs,
exported signatures, schemas — and diff each against the live implementation, reporting per
surface matches / differs (how, at file:line) / absent. Adjudicate every
differsbefore freezing: amend the contract to reality, or make the divergence an explicit migration unit with the contract as the target state. Never freeze a contradiction silently — the fidelity audit below reads source, not the repo, so it cannot catch this. - Pull design authorities into the repo before anything forks. Where the roadmap provides
designs — comps, design-system components, interaction patterns — they bind the same way a
frozen contract binds: a screen that has a comp is never built from primitives, and "matches the
design" is not an acceptance criterion a text-only agent can grade. Copy the governing material
into the repo (
designAuthorities[].path) as part of the plan-pack commit, so every unit forks with its comp already in the base — a comp the implementer cannot read is a comp it will reinvent. Put adoptable component source in the product tree, never under.roadmap/: coding agents may not write there and it is archived at close-out, so anything importing from it breaks. Where designs exist, also provision a headless screenshot capability and document its command in the brief — without one, both this audit and the per-wave design reconcile silently degrade to reading text, which is the failure that produced this bullet. Cite the binding section on each covered unit (unit.design) and in its spec, the way specs cite contracts. Hunt two plan-pack defects before dispatch, because both are an order of magnitude cheaper here than anywhere downstream: a unit whose surface an authoritycoversbut whose spec cites no section, and a spec clause that contradicts the comp it cites. The second ranks with a contract contradiction — the plan-check redirects or escalates on it rather than letting it surface as a late gate finding or a post-hoc audit. - Classify every dependency edge:
contract(the dependent needs only the interface, which you just wrote — fully front-loadable) orcontingent(the dependent's design needs the dependency's actual results — forces a wave boundary and a replan by you). Be conservative: a wrongcontractcall surfaces as a late integration failure; a wrongcontingentcall merely costs one cheap replan. - Write specs that state goal, constraints, contract references, and acceptance criteria —
not step-by-step instructions. The implementer is capable; over-specification degrades its work
exactly the way it would degrade yours. But it cannot ask you anything: the bar is that a
competent engineer could build the unit without a single question. Interrogate each spec
against that bar before dispatch — a question with a look-up-able answer is yours to resolve
now (dispatch an agent for the fact); a question that is a genuine decision is a spec defect,
settled by you and written down or put to the user. Beyond the goal, each spec therefore
carries:
- Done-when — the acceptance criteria, each an individually checkable clause ("X returns Y under Z"), because the exit gate grades them one by one and vague criteria grade noisily. At least one must be a runnable command with an expected exit status — it is also the implementer's inner-loop signal; a criterion judgeable only by reading is fine, but never the only one.
- Scope — the files the unit is expected to touch, and an explicit out-of-scope list (the adjacent mess it must leave alone, the migration that is a different unit). The harness pins scope before the first fix round and banks out-of-scope imperfections as debt rather than fixing them; what you do not scope, the implementer will either omit or wander into.
- Test seams, pre-agreed — where the unit's tests hook in. As few as possible; one is ideal. Implementers left to choose seams restructure production code to create them.
- What must be preserved — for anything refactor-shaped, the behaviour that must not change. A refactor spec without a preserve-list is an invitation to rewrite.
- Which decisions are open — the decisions deliberately left to the implementer, so that everything else unsettled is a stop-and-escalate, never a silent judgment call. Resolve spec-internal contradictions at authoring time — the codex spec-critique and the plan-check interrogate the spec itself, and what they find late surfaces as a redirect or a quarantine.
- Plan each unit's self-validation as part of the unit. Think ahead to everything the unit needs not just to do the work but to evaluate its own output — runnable acceptance checks, the provisioned environment to run them in, the commands in the brief, runtime evidence for behavior-sensitive work. Your feedback enters at the beginning (plan-check) and the end (gate); in between, the unit must check itself — the runnable check IS the autonomous implementer's own iterate-until-green signal, so a unit that cannot self-validate isn't ready to dispatch — that's a spec defect, not an execution risk.
- Plan the arc's preview. Decide how the integrated result is exercised — dev server, built
CLI, or, for a library-only arc, driving the public API/REPL (
kind: api, almost always possible) — and fill the plan'spreviewblock. The harness gives the preview its own worktree atworktreeRoot/__preview, rides it on the latest suite-green integration tip, and runs everypreviewcommand there — the user's checkout is never touched, so they can keep working and switching branches while the arc runs. Each wave's explorer then hunts what tests and diffs can't show. Declarepreview.ports(the ports the preview actually listens on) whenever you know them: they are the only listeners the harness's one-shot port sweep may kill, and an undeclared port is a port the sweep will leave alone rather than guess at.preview.startis run assh -c '<start>'inside a detached shell, soVAR=value cmdand&&chains are fine and a single quote is not (it throws at plan load — use double quotes or a package script); never prefix it withnohup/setsidyourself.preview.healthcheckis retried for ~60 s; a stack that builds before it listens and needs longer must carry its own patient loop inside that command. While provisioning: have Haiku create.roadmap/feedback/{explorer,user,triaged}/and writefeedback/user/TEMPLATE.md— a light pro forma (What I did — steps/command/URL · What I observed · What I expected · How much it matters — blocker/major/minor/idea · Where — area/page/unit) — committed with the plan pack. (Issue mode: skipfeedback/user/andTEMPLATE.md— users fileroadmap:bugissues via the template instead — but still createfeedback/{explorer,health,triaged}/for internal wave evidence.) Kill any staleworktreeRoot/__preview.pidleft by a dead arc: the whole process group (kill -TERM -- -$(cat …)), since a single-pid kill strands its child listeners. A staleworktreeRoot/__previewworktree is adopted, not recreated, so it needs no cleanup. - Seed
.roadmap/architect-log.md— your handoff brief to the boundary ladder: the decisions you made and why, a watch-list for the arc, and explicit dismissal criteria (what counts as noise a lower tier may drop without you). Opus drafts it from your Phase-0 reasoning; it commits with the plan pack. The conductor's fresh boundary agents inherit your steering only through this file, so what isn't written here doesn't reach them. It must open with a## Directionsection: where this codebase is deliberately heading, and the preferences that break ties. Every judgment surface reads it — plan-check, the exit gate and the escalation adjudicator — so it is how your taste reaches decisions you will never see. Write it to discriminate, not to inspire: preference orderings on the axes where units actually diverge ("prefer fewer public surfaces over more", "prefer explicit over inferred", "when a choice trades short-term speed for a closed door, take the door") plus explicit non-goals. "Fast and elegant" steers nothing and costs a slice of every judgment prompt. It is a tie-breaker only: it never overrides a spec or a frozen contract, and never licenses widening scope — which is also why it is deliberately absent from the implementer's brief. - Assign risk tiers (
low/med/high) and plan a small set of cross-unit acceptance tests targeting the seams between units. You plan them; schedule an early unit to write them; the merge gate runs them. - Resolve the cut line into an explicit in-scope set (ancestor-closed under the DAG), and jot
next-session notes for what falls beyond it while the context is hot. The cut line is what you will
judge DRAINED against later — once it is, relaunch with
admissions: 'closed'(see "Closing admissions") rather than letting the boundary tiers keep minting units past it.
Fidelity audit — proportionate to the source material. Your plan pack is built from
compressed extractions, and compression loss is silent: a dropped constraint resurfaces later as
a quarantine or a wrong contract, never as an error now. Once the plan pack is drafted, fan out
Opus auditors (models pinned — never bare typed agents): roughly one per source document or per
~40k tokens of material, capped at 8; skip only when the source is small enough that you read
every word yourself. Each reads its slice of the raw source against your drafted plan pack and
reports, with citations: requirements or constraints the plan fails to record, contradictions
between plan and source, and design decisions of pertinence that deserve to be written down. You
adjudicate every finding — amend the plan, record it in .roadmap/constraints.md, or dismiss it
with a stated reason — and fold anything genuinely ambiguous into the user question batch. The
audit verifies plan-against-source, never plan-against-repo. Where designAuthorities
exist, the comps are source too: audit the plan against them on the same footing as the written
roadmap, with auditors that can actually see the renders. A UI spec that has drifted from its comp
is compression loss of exactly the kind this audit exists to catch, and it is invisible to an
auditor reading prose alone.
Record cross-cutting constraints in .roadmap/constraints.md — design decisions and
constraints from the source that aren't interface contracts (performance budgets, technology
choices, compliance rules, explicit non-goals). The file is a numbered rulings ledger, not
free prose: each entry is C-<nn> — <one-line rule> followed by one provenance line (who ruled,
when, why). Ids are stable forever — never renumbered, never reused; superseding a ruling is a
new ruling that names the old one. Specs cite the rulings that bind them by id, your
architect-log dismissal criteria reference them by id, and the boundary tiers check asks against
them mechanically — settled questions stay settled across sessions only if the id they were
settled under cannot drift. (Arc-observed: an improvised C-nn ledger became the most load-bearing
document of a multi-week arc; this shape is now mandatory, not emergent.)
Two mechanical outputs matter more than they look: persist the recon brief
(.roadmap/brief.md — commands and conventions; the harness feeds it to every per-unit agent so
nothing guesses the test command), and fill the plan's provision block (env files to copy,
setup command) — a fresh worktree has no deps, and an unprovisioned environment fails test gates
for non-code reasons and quarantines innocent units.
Units build from commits, not from anyone's working tree. Every unit worktree forks from the
integration tip — a commit — while your recon agents read the live working tree. So before
dispatch: (1) commit .roadmap/; (2) reconcile any dirty or untracked state — anything
uncommitted that units need must be committed first, or your plan describes code the worktrees
cannot see, silently; (3) enumerate gitignored runtime files into provision.copy — have a
Haiku agent cross-check .gitignore against what the build/test commands actually read, rather
than guessing.
In issue mode, stand up the tracker (Haiku; exact labels/markers in reference.md). Create the
roadmap:*/status:*/risk:*/severity:*/debt:* labels (gh label create; ignore "already
exists"), the arc milestone, and one arc tracking issue (roadmap:arc — body: plan summary +
DAG + a <!-- roadmap:status -->…<!-- /roadmap:status --> region the wave-tail sweep fills with a
unit task list (- [x]/- [ ], checked when closed → native progress rollup) + a session-report
placeholder; record its number in plan.trackingIssue). Open one roadmap:unit issue per in-scope
unit (body's first line the <!-- roadmap:unit id=<id> --> marker, then the spec) and a thin
status:backlog issue per deferred unit. The unit issue is where the spec is authored, but you
still snapshot it into .roadmap/specs/<id>.md and commit — units build from that frozen snapshot,
never a live issue. If .github/ISSUE_TEMPLATE/roadmap-bug.yml / roadmap-unit.yml are absent
on the default branch, add them (reference copies live in this skill's templates/) on a branch and
open a small PR the user merges — planning continues meanwhile; the templates are only needed by
the first wave boundary. That PR is the only pre-session-end touch of main, and only the user's merge
moves it (invariant 5 intact).
Persist everything under .roadmap/ (shapes in reference.md). Write plan.json and state.json
however your serializer likes — every JSON escape survives the launch read intact — but keep both
small: prose belongs in a file under .roadmap/ that the document references by absolute path,
because one courier has to copy each of them at every launch and resume. Then stop and talk to the
user: present the decomposition, contracts, cut-line interpretation, and your questions —
batched, once. Discipline the questions: only ask what you couldn't resolve yourself, rank by
impact × uncertainty, cap around five, and attach your recommended answer to each so the user can
mostly confirm. Also tell them two things concretely: where the preview will be reachable
(preview.howToAccess, served from its own worktree at worktreeRoot/__preview — their own
checkout is untouched, so they can keep working in it), and the absolute path of
.roadmap/feedback/user/ — they
can copy TEMPLATE.md there at any time; notes are batched into your next triage, never injected
mid-run. (Issue mode: instead, point them at the roadmap-bug issue template to report bugs
and roadmap-unit to propose new units — both are read at your next boundary (and open roadmap:bug
issues again at the next Phase 0 between sessions), never injected mid-run.) Get approval before dispatch. If invoked with --dry-run, stop here; the plan pack is
itself a deliverable.
Phase 1…n — Execute waves
Launch the conductor in the background and stay quiet — it notifies you when the whole run
finishes, not each wave. Do not read plan.json or state.json first: the envelope names the
directory and the script reads the pack itself, cksum-verified, on a floor-tier agent. Pasting those
documents into args put the whole pack through this session — the most expensive tier in the
system — on every launch and every resume.
Workflow({ scriptPath: "<this skill's directory>/conductor.mjs",
args: { roadmapDir: "<repoPath>/.roadmap",
config,
harnessPath: "<this skill's directory>/harness.mjs",
launchId: "<a value you have never used before — a timestamp is fine>" } })
roadmapDir and harnessPath are both required — the first is the pack the script reads at launch,
the second is the child script it dispatches each wave with. launchId must be FRESH on every
launch and on every resume —
never reuse one, never derive it from the arc or the wave. It is how the scripts keep environment
probes (the launch pack read itself, provisioning, integration setup, the merged/reachability git
probes, the per-wave codex probe, the host preflight, the preview worktree + mirror couriers) out of
resumeFromRunId's cache: those probes answer "what does the disk and git look like right now",
and a replayed answer is a lie (a resume once replayed a pre-rebuild cd: No such file and
quarantined healthy units). The scripts cannot generate it themselves — Date.now() and
Math.random() do not exist in a workflow script, so it has to arrive in args. Omitting it does
not fail the run: the harness records one no-launch-id degradation and runs the probes unsalted. Record the returned runId and scriptPath into state.json's
optional run field at launch: that runId identifies the whole multi-wave run, so a
same-session resumeFromRunId replays every completed wave and crash forensics are one cat
away.
After every run — persist
The scripts write no STATE under .roadmap/. They have no filesystem, so every byte of it used
to go through a model transcribing a document — the second-largest model cost in the system, and it
occasionally lost the document anyway. State, the merged plan, the debt and log sections and both
event ledgers now ride home in the return value, and one command turns them into files at zero model
cost. (What the scripts still put there is content a model actually authored: a spec, a quarantine
dossier, a boundary role's own report, and the feedback move-feedback archives.)
node <this skill's directory>/persist.mjs \
--run <the run's workflow transcript directory> \
--script <this skill's directory>/conductor.mjs \
--args '<the exact envelope you launched with>'
Run it after every Workflow return, and after every crash — before you read .roadmap/ for
anything, and before any relaunch. It replays the run against its own journal (no model is called),
then writes state.json, plan.json, debt.json, the debt.md and architect-log.md sections,
skill-degradations.md, and appends degradations.jsonl / escalations.jsonl. Re-running it over
the same run is a no-op, so persisting twice is safe.
Read its last line:
-
OK …(exit 0) — everything landed; the files named on that line are current. A trailingremoved=state.partial.jsonmeans an earlier refusal's parked prefix was stale once this whole state landed, and was deleted. -
PARTIAL stoppedAt=<label>(exit 2) — the replay ran out of journal, i.e. the run died at that call.state.jsonis the last snapshot the run logged, markedpartial: {stoppedAt}. Work the recovery ladder below, then persist again. -
PARTIAL-REFUSED stoppedAt=<label> why=<divergence|newer-on-disk>(exit 2) — the partial would have REGRESSEDstate.json, so it was parked instate.partial.jsonandstate.jsonwas left untouched.why=divergencemeans the miss was(out of journal order): the replay diverged, the run did not fail — the run's own returned state is further along than anything replayable.why=newer-on-diskmeansstate.jsonalready holds a later wave, or the same wave written whole. Either way, persist the value the run returned (it is in the task output) —node <this skill's directory>/persist.mjs --returned <that value, as a .json file> \ --args '<the exact envelope you launched with>'which skips the replay and writes every document from that value (
--run/--scriptare not needed;--argsstill is). Then investigate the divergence — a script edited since the journal was written is the usual cause. Never relaunch fromstate.partial.json, and never hand-editstate.jsonin its place: the ledger appends, debt sections and log entries only land if the persister writes them. -
PLAN-CONFLICT unknownUnits=…—.roadmap/plan.jsonholds unit ids this run never saw (a root edit between launches, a hand-merged respec). The file was left exactly as it was; merge the two plans by hand before relaunching. (This fires on the--returnedpath too.) -
exit 1 — nothing was written and the reason is on stderr. The most common is a partial with no snapshot at all (the run died before its first status change): relaunch and persist again.
Do not pass config: { boundary: 'off' } to end the arc — arc-completeness is detected
post-hoc, and the final wave's untriaged boundary evidence is handed to you deliberately as
integration-review input. (You may pass it on a relaunch you know is final. Owed boundary jobs
still run in that wave — an explorer or design reconcile that never ran is a debt the last boundary
pays, not one it defers to a boundary that will never come.)
Closing admissions. Once you have judged the plan DRAINED — every unit you meant to build is
terminal, and what is left arriving is polish — relaunch with
config: { conductor: { admissions: 'closed' } }. Under it the boundary tiers still run and still
judge, but they may not mint units: every draft they would have admitted and every unit they would
have promoted is banked to the debt ledger with its origin, and the arc closes on the next dry
boundary. Without it, a healthy assessor drafts something every wave and the denominator grows as
fast as the numerator — arc-observed, ~93% merged for 12+ hours after the plan was already drained.
The one thing closed does not bank is a finding graded blocker: that routes to the Fable
boundary tier for a ruling instead of being auto-admitted.
Between waves the conductor triages each boundary through a tiered ladder — script, then Opus,
then Fable — escalating only as far as the boundary demands, and returning to you only for the
calls that are yours. The ladder's routing table, config knobs, and the per-unit pipeline the
harness runs are in reference.md. What you need at the keyboard is what comes back.
Fallback — per-wave harness dispatch. You can still launch harness.mjs directly per wave
(args: { roadmapDir, config, launchId }, no harnessPath — it reads the same pack, and
persist.mjs --script harness.mjs writes its return) and triage every boundary yourself; setting
boundaryTriage: 'root' gets the same effect without leaving the conductor. If you take the
fallback path you inherit the conductor's duties back — in particular withholding contingent
dependents (reference.md), which the harness's scheduler does not do for you.
When the conductor returns
Judgment returns to you with status: 'conductor-return', a reason, and the returned state.
On every wake, first read two things: .roadmap/architect-log.md (the ladder's journal — what
the boundary agents decided in your stead, and why) and the returned state's boundary/debt
residue. That residue is intact on a terminal boundary; on a continuation boundary the
conductor already banked the wave's debt (.roadmap/debt.md, or roadmap:debt issues) and cleared
only what the banker confirmed — anything it did not name stays in state.debt, re-banks next
boundary, and carries a debt-unbanked degradation. Either way the wave's debt is already on disk at
.roadmap/debt.json, written the moment the wave returned. Then act on the reason:
arc-complete— the boundary yielded no further work; the arc is at its cut line. Go to Session end. The final wave's boundary evidence rode back untriaged, deliberately. Anystuckids are in-scope units wedged behind an unresolved quarantine — adjudicate them before closing.arcSummaryis the tally to report from:{merged, quarantined, blocked, deferred, pendingFeedback, wavesRun}— readblockedas well asquarantined, since a unit whose tooling never came back is neither built nor failed and appears in no other bucket.arc-stalled— a tier called the arc done while in-scope, dispatchable units remained (outstanding). The tier was wrong, not the plan: confirm the units are still wanted and relaunch. Arc-observed — this fired twice before the census existed, caught only by hand.contract-amendment— a frozen-surface mismatch the ladder may not resolve. Amend the contract to reality, or spec the divergence as an explicit migration unit, then relaunch.contingent-replan— a contingent edge crossed, or withheld dependents are the only work left. Read the learnings, revise the downstream specs, relaunch.needs-user— a call only the user can make; the question is in the escalating agent'snotes. Get the answer, fold it in, relaunch.plan-cycle— the merged plan's dependency graph closed a loop, so no wave could be dispatched (the harness throws on a cyclic plan, which would take the whole run down).edgesandunitsname the loop. Remove or repoint one edge in.roadmap/plan.json, then relaunch — the boundary's specs, plan, issues and debt were all staged before the return, so nothing is lost.max-waves/agent-budget— the run hit its wave cap or its pre-dispatch budget guard with work remaining. Persist, then relaunch fresh (a new run resets the per-run agent counter).max-wavescarries the final wave'sboundaryback markedtriaged:true— read it for context, but its findings are already banked and its feedback already moved, so it is not yours to triage again.boundary-degraded— the boundary phase was enabled but produced nothing (every job failed). Spawn the explorer/health agents yourself, triage their output, then relaunch.triage-degraded— the boundary evidence is good but the triage agent itself died (a terminal API error). Nothing was admitted or dropped. Triage this boundary by hand, as forboundary-degraded, then relaunch.- a halt reason (
codex-unavailable,codex-usage-limit,env-pids-exhausted,env-no-reaper,env-verify-blocked,platform-outage) — the wave stopped dispatching and handed you a resumable pause, not a failure: nothing was quarantined, the units inparkedkeep their commits and re-enter by adoption. Each has exactly one human action — re-auth (codex login), wait out a usage-limit, platform-outage or Codex-backend-outage window, or fix the box (a full pid cgroup and a ≥ 1000-zombie backlog both mean: recreate the container with a reaping PID 1;env-verify-blockedmeans the verifiers' own tooling could not run at all — read their failure output in theverify-blockeddegradations, then fix the registry, network or missing global tool).codex-unavailablecovers two of those, so read the degradation'swhatbefore acting: a failed--version/login statusis a re-login, while a failed exec smoke or a tripped backend breaker (≥2 consecutiveturn.failedruns on different units or roles, same HTTP status) is the provider — no login helps, wait. Do the action, then relaunch; never route around a halt by re-implementing the work another way.env-verify-blockedis the top rung of a three-rung ladder, and the lower two need no wake at all: the FIRST verify a unit's tooling blocks (or a verify role that never ran,verify-unrun) leaves that unitblocked— commits intact, no dossier, nothing judged about the work — and the next wave's start loop re-opens and re-verifies it; a SECOND block on a later wave quarantines it with an environment dossier; two DISTINCT units blocked in one wave is the host fact that halts here. Soblockedin a returned state is an ordinary, self-healing outcome rather than something to adjudicate — the conductor'sarcSummarygives it a bucket of its own besidemerged/quarantined/deferred, and a unit still sitting there when the arc closes is one whose environment never got fixed. root-triage— you setboundaryTriage: 'root', so every boundary returns to you.
Nothing to replan? Just relaunch the conductor. Keep your own turns terse — on a relaunch wake you are a dispatcher, not a narrator. "preview at X; 6 feedback items: 4 actioned, 2 dismissed" is plenty.
On the wakes where you do triage
That is root-triage, boundary-degraded, and the final wave's evidence at Session end. The
harness has already run the boundary jobs (a codex runtime explorer against the live preview, a
codex health assessor against the integration tip, codex full-suite flake re-runs); their results are
in the returned state's boundary block, and each role wrote its own
feedback/{explorer,health,design}/wave-<n>.md (design/ appears only on waves that merged a
design-cited unit; the flake band's record is feedback/health/wave-<n>-flake.md). If that
block is absent, every job failed or the phase was off — only then spawn the agents yourself.
- Quarantines: read the dossiers in
.roadmap/quarantine/— the reason routes the action. Environment/tooling-blocked → fix provisioning or the brief and re-run as-is. Unsatisfiable-spec → respec or amend the contract. Everything else → redesign: split the unit, revise its spec, raise its budget, or mark it for the user. A redesigned unit re-enters as a new spec; never re-run one under the spec that failed. - Triage the boundary block, the
.roadmap/feedback/user notes (issue mode: the openroadmap:bugissues), and the wave'sdebtarray together — once, at this boundary. Fold items into revised specs; cut fix units into the next wave; treat contract-contradicting feedback as a contract amendment (yours alone); or dismiss with a stated reason. The health assessor's consolidation fix-unit drafts are the default action, not a suggestion: admit them unless you see a reason to cut. Your judgment enters as a veto over noise, not as authoring each from scratch — that is what keeps cross-unit drift from dying unactioned in a folder. They run the identical isolation → gate → merge pipeline as any unit, so admitting one costs no safety. - Debt you choose not to fix this wave doesn't vanish. With the arc still running planned work,
the default is to fold even minor debt into the next wave as consolidation fix-work rather than
bank it (the debt rule — the conductor's tier-2 does this for you); only genuinely below-the-cut-line
debt banks — and every banked item carries its closed-set
bankReason(out-of-scope-file · needs-migration-or-ruling · pre-existing-untouched), because "minor" alone is never a reason to bank and correctness findings can never bank through a gate approve. Banked debt goes to the living.roadmap/debt.mdledger (or, in issue mode, ONE consolidatedroadmap:debtissue per unit-residue, keyedarc=<tracking issue or milestone> wave=<N> unit=<id>; a consolidation fix-unit that resolves specific issues names them in itsclosesfield so the merge path closes them); annotate an entry resolved when a fix unit lands. Sonnet-compress the batch first if it's large; findings at a superseded sha are discounted, not re-litigated. Consumed feedback moves tofeedback/triaged/<wave>/(issue mode: theroadmap:bugissues are closed with a disposition comment). Triage silently — contact the user only for a critical call you genuinely cannot make.
When the skill itself misbehaves — .roadmap/degradations.jsonl
The scripts' safety nets are silent by design: a dead agent degrades to a coded fallback so a blip never costs an arc. That silence is dangerous — it once let a deterministic bug masquerade as three runs of "network flakiness" — so every degradation is now recorded, not swallowed. Read it.
Every return carries a degradations array (this run's rows). persist.mjs appends each one to
.roadmap/degradations.jsonl — the arc's full record — and renders a per-kind count summary to
.roadmap/skill-degradations.md. skill-feedback.md is yours and the user's: nothing in the
orchestrator can write it. Each entry is
{script, wave, phase, label, model, kind, what}. The kind you will meet most are schema-retry (a
report was rejected and retried), no-report (the agent died and agent() returned null — the
platform does not expose why), salvage-failed and threw; reference.md enumerates the full set,
including the halt kinds, the codex kinds, and the refusals below.
Your duties:
- Empty array — say nothing. A clean run needs no commentary.
- Non-empty — read it before you trust the wave. A degraded agent means some judgment you were
promised did not happen. A
no-reportat a gate or a triage tier is not cosmetic: that unit or boundary got the fallback, not the verdict. - Diagnose
no-reportfrom the transcript, not by guessing. The entry names the agent'slabel; find it in the run'sagent-*.jsonland read the last entries. The real error is there and nowhere else. Do not attribute it to the network without looking — a repeated failure at the same label is a bug in the skill, not weather. - A repeated
schema-retryon one label means a cap is wrong, not that the model is verbose. - Carry it upstream.
.roadmap/skill-feedback.md(hand-written),skill-degradations.mdanddegradations.jsonl(machine-written) are living documents — they are about the orchestrator, not the product, so they never go indebt.mdand are never archived with the arc. Add your own observations toskill-feedback.md; report all three at Session end and tell the user to take them to the skill's own repository. This is the only channel by which the skill learns from its own failures.
If a run dies mid-run — recovery ladder
The conductor and its child harness share one journal, so resumeFromRunId replays every
completed wave and every completed unit within the in-flight wave for free — but it is
same-session only, even when the crash notification recommends otherwise (that recommendation
is wrong across sessions; the journal does not survive the host process).
Run persist.mjs first, always (the command above, with the same --run directory and the same
envelope). A crashed run leaves no state on disk by itself; the persister is what turns whatever the
journal holds into a state.json, marked partial: {stoppedAt: <label>} when the replay could not
reach the end. That file is what rung 3 relaunches from. If it answers PARTIAL-REFUSED instead,
the replay diverged or disk is already ahead — re-run it with --returned <the run's return value>
(above) before you touch the ladder, so rung 3 relaunches from the run's real state. Then work the
ladder in order:
-
Same session, run still alive — nothing to do; it will notify you when the run finishes.
-
Same session, run dead —
resumeFromRunIdwith thescriptPathrecorded instate.json'srunfield. Best-effort: if it doesn't cleanly resume, drop to rung 3. -
Adopt rejected, or a new session — launch a fresh conductor against the
state.jsonpersist.mjsjust wrote. This behaves like a resume, not a restart: a continuation boundary snapshots the consumed state, so a partial persist lands the last completed boundary; and within the in-flight wave, the harness asks git what already finished before it dispatches anything (a branch that landed on the integration branch is recordedmergedand never re-dispatched — including one the persisted state still records asrunning/merge-ready; a crashedrunningunit whose branch did not land auto-adopts its committed work and re-enters at verify — itsstagefield andgit log unit/<id>show how far it got). The loss bound is only the in-flight wave's uncached agent calls. Pass a freshlaunchIdon the relaunch: it is what stops those git/disk probes being served from the dead run's cache.A wave that halts immediately with
integration tip regressedis not a crash — it is the harness refusing to build on a branch its own record cannot reach (the branch was rewound, or merges landed on a detached HEAD and are dangling). Nothing was changed. Find the merges (git reflog <integration branch>,git fsck --unreachable), decide which history is real, point the branch at it, and setstate.json'sintegrationTipto match before relaunching.Two more refusals read the same way — the run declining to destroy something rather than failing. A
plan-conflictdegradation means.roadmap/plan.jsonon disk holds unit ids this run has never seen, sopersist.mjsskipped the write instead of overwriting them (it says so on stdout too); merging the two plans is yours before you relaunch. Adebt-unbankeddegradation means the issue-mode banker did not confirm every item it was given: the unconfirmed ones stay instate.debtand.roadmap/debt.jsonand re-bank at the next boundary, so nothing is lost — but a repeat at the same wave means theghprojection is failing, and thegh-syncentries beside it are the thing to read.A
pack-unreadablethrow at launch is not a crash either: the courier could not produce a copy ofplan.jsonorstate.jsonmatching the file's owncksum, twice, so the run refused to dispatch a wave from a document nobody could vouch for. The throw names the file, its byte count and how much arrived. JSON escapes are not the cause — the read command rewrites every backslash to@bs@before the copy and the script reverses it, so\",\\and\uXXXXtravel intact. The three real causes, in order of likelihood: the file is too big (a copy that stops far short of the byte count — getstate.jsonback under ~35 KB by moving prose into files and referencing them by path);roadmapDiris wrong or the file does not parse; or the document genuinely contains the literal text@bs@, which the reversal would corrupt and thecksumtherefore rejects — remove it. Fix, then relaunch.A branch with commits beyond its fork base that the passed state does not mark
runningis refused, not overwritten (has-commitsquarantine, branch intact) — adopt it deliberately viaunit.existingBranch, or delete the branch yourself. A self-referentialexistingBranch(the unit's ownunit/<id>) is refused at plan validation; anchor the commits under a differently-named ref (e.g.adopt/<id>) and adopt that.
Before any relaunch, kill the stale worktreeRoot/__preview.pid process group
(kill -TERM -- -$(cat …)), not just the leader.
Session end
- Integration review — yours, guaranteed. With the final state, contract amendments, the
quarantine list, any untriaged feedback,
.roadmap/architect-log.md(re-examine every tier-3 respec and boundary dismissal the ladder made in your stead), and the final wave's untriaged boundary evidence in hand, read the integrated diff on the integration branch and judge cross-unit coherence — the one thing no per-unit gate could see. Hand any findings to Opus fixers as directives. If the final state carries a non-emptyowedarray, those boundary jobs never ran: discharge each (run the job yourself against the final tip) or waive it explicitly in the architect log — an owed job silently dropped at close-out is exactly the skipped-reconcile failure the marker exists to prevent. - Report plainly: merged / quarantined (with dossier pointers) / blocked (the
arcSummarybucket: verified-tooling failures whose environment was never fixed) / deferred beyond the cut line; feedback actioned / dismissed / pending (pending goes into next-session notes); the debt ledger's state; where the run's attention actually went — Claude spend broken out by tier (spend.fable/opus/sonnet/haiku) besidespend.codexandspend.codexRuns, so the Claude-versus-codex ratio is on the page and not left to be inferred; gate spend broken down by first-pass gate (spend.opusGateRounds, whatever tiergateModelsent it to) vs escalated Fable gate (spend.gateRounds), and consult spend; the conductor's ladder breakdown from the final state'sconductorblock plusspend.boundaryTriages/spend.boundaryFables; any.roadmap/degradations.jsonlentries, and which stage's judgment they cost you; notes for the next session. Partial completion with honest dossiers is a good outcome — a silent one is not. Census every continuation brief before you trust it. Next-session notes, a continuation brief, an architect-log summary — any hand-compressed handoff is subject to the same silent loss the fidelity audit exists for, and small errors there (a stale tip sha, an off-by-one unit count — both arc-observed) cost real friction at resume. After writing one, dispatch a single Haiku census: compare the brief's claimed integration tip, unit counts, unit ids, and pending/quarantined sets againstplan.json/state.json, and report every mismatch. Fix the brief (or the state) before ending the session — mechanical, one call, catches the class. - Deliver for merge. File mode: ask the user before fast-forwarding
mainto the integration branch. Issue mode: open one integration PR (integration branch → default branch) whose body summarizes the arc and listsCloses #<issue>for every merged unit, so merging it auto-closes those issues; the user's merge of that PR is the invariant-5 confirmation. - Close out the arc.
.roadmap/is arc-scoped working state, not permanent documentation — left raw, a later run reads the stalestate.jsonand forks worktrees from a dead integration tip, and retired "frozen" contracts masquerade as binding. Aftermainadvances: stop the preview process (kill the whole group recorded inworktreeRoot/__preview.pid) and remove its worktree (git worktree remove --force worktreeRoot/__preview); archive the arc (plan, brief, specs, contracts, state,architect-log.md, dossiers, feedback — triaged and pending alike — report) into.roadmap/archive/<date>-<cutline>/in one commit; keep the living documents (constraints.md,debt.md,skill-feedback.md,skill-degradations.md,degradations.jsonl,escalations.jsonl, notes) at top level — unresolved debt is a first-class input to the next arc's Phase 0, and the skill-defect record belongs to the skill, not this arc, so archiving it would bury the only account of how the orchestrator failed; remove unit worktrees and mergedunit/*branches (keep quarantined branches — their dossiers point at them), and sweepworktreeRoot/__codex/with them — the codex briefs/events/session artifacts are per-arc forensics whose value ends at close-out (keep a quarantined unit's__codex/<unit>/alongside its branch if its dossier cites it); delete the integration branch once merged. In issue mode also: post the session report to the arc tracking issue and close it; close the milestone; verify merged-unit issues are closed-completed and deferred ones closed-not-planned (the wave-tail sweep usually did this); and leave open theroadmap:debtissues and anystatus:quarantinedunit issues — they are the next session's inputs, the issue-mode counterpart of the living docs. The absence of a top-levelstate.jsonis the unambiguous "no arc in flight" marker the next run keys on.