Choose the orchestration route
Default: Codex orchestrates; gpt-5.6-luna does delegated work. In Codex,
read codex-orchestration.md and execute that workflow.
It replaces the external-session workflow below, including its dispatcher,
Claude session key, pane ledger, and shell wait bridge. Keep the parent Codex
model unchanged; select Luna explicitly when spawning each worker or reviewer.
Use the external-session workflow below only when the user requests external builder sessions or runs this skill outside Codex. If native delegation or Luna is unavailable, surface the limitation; do not silently change model or route.
External-session workflow
You are the ARCHITECT/ORCHESTRATOR. offload is your single-turn engine;
the BUILDER is whichever harness ~/.borkweb-skills/config.json resolves to
(offload's harness.mjs picks it, with quota-aware failover). You never write
implementation code — you spec
slices, judge raw gate numbers, and drive the loop to merge-ready. The human is
the final judge and the one who merges, unless the goal explicitly told you to.
/complete <goal> wraps offload (one architect turn) in a loop. Everything
offload does — handoff, arbitration, gate judging, builder block, dispatch —
still happens; this skill adds the loop, contextual plan reviews, council policy,
the wait bridge, the slice ledger, and the final readiness gate.
Progress is read from board, never from memory. A handoff doc is one
builder's mailbox; it cannot tell you the state of the other five, and it has no
way to say "this slice is finished." The ledger can. Every claim you make to the
human about what is running, done, or stuck comes from the board output of
that same turn.
1. Resolve goal + endpoint
The default endpoint is merge-ready, NOT merged. Build every slice, pass each
per-slice review, and land a clean final integration review — then STOP at the
edge of merge and hand to the human with the branch and the exact merge command.
Only set the endpoint to merged when the user's instructions explicitly authorize it — phrases like "merge into main", "and merge", "land it", "all the way to main", "merge it". A bare "complete", "to completion", "done", "ship it", or "run the loop" does not authorize a merge → stop at merge-ready. When in doubt, do not merge. Hold the goal text verbatim — you check remaining scope (and whether merge was authorized) against it every turn.
2. Open the ledger — first, and before any slice exists
node "<…>/handoff.mjs" ledger init "$PWD" "<the goal, verbatim>"
This is the architect's own file, one per session, and builders never write
it. It is the only place a slice's terminal state lives. Each slice also gets
its own handoff doc (handoff.mjs init "<worktree>" "$CLAUDE_CODE_SESSION_ID"),
which is the builder's mailbox — spec in, gates out. Two different records; do
not conflate them.
Register a slice the moment you spec it, and update it the moment its state changes:
node "<…>/handoff.mjs" ledger add "$PWD" --id 3 --title "inline axis widths" \
--branch fix/inline-axis-widths --slice-cwd "<worktree>" --handoff "<doc>" --state specced
node "<…>/handoff.mjs" ledger set "$PWD" 3 --state dispatched --pane wY:p22
node "<…>/handoff.mjs" ledger set "$PWD" 3 --state accepted \
--verdict "SAFE TO LAND · unit 2312/0/0 · integration 10/0/0" --pr <url>
States: specced → dispatched → blocked | results-ready → accepted | rejected,
plus merged and abandoned. The four terminal states are the point. A
slice you have accepted, rejected, merged or dropped MUST be written terminal in
the same turn you decide it. Skip that and it keeps reading as in-flight — which
is exactly how finished slices get reported as "still running" and how a slice
whose builder exited sits unowned for an hour with nobody on it.
3. Contextual plan review (before the first gates are frozen)
Pick the review(s) by what the first slice touches — more than one can apply:
- architecture / data flow / concurrency / migrations →
plan-eng-review - UI / visual / interaction →
plan-design-review - developer-facing surface (a contract someone else builds against: API, CLI,
SDK, library, public interface, docs) →
plan-devex-review
Trigger devex on exposed/consumed interfaces, not on code that is merely a CLI
internally. Skip reviews entirely for a trivial single-slice goal — say so. Fold
review outcomes into the slice spec you hand to offload.
4. Confirm autonomy posture — ONCE, up front
Ask the user a single time (AskUserQuestion):
- Dispatch: auto-dispatch each slice to the configured builder without re-asking, or pause for an OK each turn?
- Merge — ask this only if the goal authorized a merge (step 1): once the final review is clean, auto-merge, or pause for an OK first? If the goal did not authorize a merge, skip this question entirely — the endpoint is merge-ready and you will stop there regardless.
State the safety fact plainly: dispatch launches the builder with its permission
gates relaxed — codex --dangerously-bypass-approvals-and-sandbox (sandbox fully
off), opencode all-permissions config, grok --always-approve, claude
--permission-mode auto interactive / full bypass headless — with full local
access, merely launched from the repo dir. Name the flag for the harness actually
chosen. Record the chosen posture; honor it for the rest of the loop.
5. The loop — slices run concurrently; the board is the state
Builder launch invariant — dispatch.sh only
Every builder launch MUST go through offload and its dispatch.sh. Concurrency
does not change the launcher; it only means several independently dispatched
slices may be alive at once. Invoke offload once for each slice and let its
documented dispatcher resolve the harness and frontend.
Never create a builder with raw herdr agent start, herdr agent spawn,
herdr pane split, tmux new-window, Terminal, or a headless harness command.
Those paths bypass offload's handoff marker, activity/turn-end wiring, duplicate
builder guard, harness failover, and herdr-tab guarantee. Raw herdr commands are
only for supervising a builder that dispatch.sh already launched: reading its
pane, sending a ruling, focusing its tab, or resuming its existing process.
Treat the dispatcher's success line as a postcondition before advancing slice
state. Inside herdr it must name new herdr tab <tab_id>, the workspace, and the
root pane. A pane id without a tab id is a failed dispatch. Do not set the slice
to dispatched, record its pane in the ledger, or arm its wait bridge; surface
the launch failure and correct it through offload.
This invariant is enforced, not just stated. ledger add/set --pane checks
the pane against dispatch.sh's signature — a herdr tab labeled builder holding
exactly one pane — and refuses a pane that lacks it, because a hand-started
builder has no builder marker, no activity sidecar and no turn-end hook, so its
wait bridge can never fire. board re-checks every live slice and flags
UNMANAGED PANE. If you hit the refusal, the answer is to re-dispatch through
offload, never OFFLOAD_ALLOW_UNVERIFIED_PANE=1 — that override exists for a
frontend the check cannot see, not for routing around a failed dispatch.
Per-slice loop
Slices that touch disjoint files run at the same time. There is no per-slice turn-taking: each dispatched slice has its own worktree, its own handoff doc, and its own wait bridge, and any of them can wake you. What makes that tractable is that you never hold the fleet in your head.
EVERY WAKE, FIRST THING — no exceptions, including the wake that follows a
compaction:
node "<…>/handoff.mjs" board "$PWD"
Act on what it prints, in this order:
! NO OWNER → a dispatched slice whose builder exited with no bridge
armed. NOBODY IS WORKING IT. Decide now: judge what it
produced and mark it terminal, or dispatch a corrective
onto the same branch. Never leave a wake without
resolving every NO OWNER row.
! NEEDS ARCHITECT → arbitrate (blocked) or judge the gates (results-ready).
! MISMATCH → the builder's doc moved past your ledger. The doc is the
builder's word — act on it, then bring the ledger level.
! CORRUPT DOC → rewrite the status; until then that slice's bridge is deaf.
! DOC MISSING → STOP. Do not re-dispatch blindly; surface it.
! UNMANAGED PANE → the recorded pane did not come from dispatch.sh. That
builder is unreachable by the bridge whatever its pane
shows. Judge whatever it produced, mark the slice
terminal, and re-dispatch through `offload`.
(no flags) → nothing to do on that slice.
Then, per slice:
a. Invoke the `offload` skill for ONE architect turn on the slice that needs it.
It arbitrates, judges any ready results, specs the next slice (freezing gates),
emits the builder block, sets status=dispatched, and dispatches the builder.
Honor the dispatch posture from step 4 — in pause mode, get the user's OK
before offload dispatches. Dispatch every slice that is ready to run and
shares no files with one already in flight; only serialize on real conflict.
"Dispatch" here always means offload's `dispatch.sh` path — never substitute a
raw frontend or harness command.
b. After each valid dispatch — including the required tab-id postcondition inside
herdr — `ledger set … --state dispatched --pane <pane>` and launch that slice's
WAIT BRIDGE backgrounded (below). Then stop your turn — do NOT poll, do NOT
ScheduleWakeup. The harness re-invokes you when a bridge exits.
c. On wake, run `board` (above), then read the background task's final `WAITER:`
line and branch:
WAITER: ready → go to (d)
WAITER: builder-blocked … → the builder flipped the handoff to `blocked`: it stopped on a
mid-slice blocker and is waiting for a ruling. ARBITRATE NOW —
follow "Arbitrating a stopped builder" below. This is not a
judgment call and not a reason to wait.
WAITER: builder-awaiting-input … → the harness's own turn-end hook reported the builder stopped
without flipping the handoff (blocked but didn't say so, hit a
permission prompt, or finished without running `ready`).
Same mandate: "Arbitrating a stopped builder" below.
WAITER: builder-idle … → no handoff flip, no turn-end, no session-log activity, no CPU.
The idle detector now counts harness session-log writes as life,
so a quiet verdict means the builder is not thinking, not
streaming, and not writing — do NOT rationalize it as "just
slow" and do NOT relaunch the bridge on a hunch. Treat it
exactly like builder-awaiting-input.
WAITER: builder-exited-without-ready → surface as a BLOCKER, pause, ask human
WAITER: timeout … → surface, pause, ask human
WAITER: never-started … → no builder ever appeared: read the dispatch output for a launch failure, fix the cause, re-dispatch. Surface as a BLOCKER if it repeats
WAITER: handoff-deleted … → the handoff vanished mid-wait: STOP. Do not re-dispatch blindly — resolve the handoff path first, then ask the human
d. Run the `offload` engine again (step a) — its step 0 picks up status
results-ready and judges the raw gates against the frozen gates. Read the
verdict it produces.
e. Branch on the verdict. offload's per-slice verdict now bundles an independent
`review` acceptance check, so a DO NOT LAND counts as a failure here:
gates or review fail → `ledger set … --state rejected --verdict "<why>"`;
relay defects; offload specs a corrective slice → (a)
gates pass + review clean → `ledger set … --state accepted --verdict "<raw numbers>"`
any scope left → offload specs the next slice → (a)
goal met across all slices → go to step 7 (finish)
Write the terminal state in THIS turn, before you report anything to the human.
You are still inside this loop across every harness wake — a WAITER: line in a
background result means resume here, not start over.
Reporting progress to the human
Every status you give — a table, a "still running", a "two are done" — is
assembled from the board output of the current turn. Do not narrate slice
state from what you remember dispatching; that memory is what goes stale, and it
does not survive a compaction. If a claim is not in this turn's board, either
run board again or say you do not know.
A reversal counts as a state change: if you accepted a slice and later found it
was wrong (CI red, a regression your gates missed), set it back to rejected
with the reason in the same turn you learn it. An accepted row that is no
longer true is worse than no row.
Arbitrating a stopped builder (builder-blocked / builder-awaiting-input / builder-idle)
The lesson this procedure encodes: a builder once sat self-reported-blocked in its pane for ~2 hours because the only wake signal was "results ready" and the idle signal that DID fire was reasoned away as "probably just slow". The wake is deterministic now; so is the response. In order, no steps skipped:
- Read the builder pane (
herdr pane read <pane>/tmux capture-pane) and the handoff's## Open disagreements(handoff.mjs section get "$HANDOFF" "Open disagreements"). - Rule on every open item — accept / reject / modify, with one-line reasons —
and record the rulings with
handoff.mjs section append "$HANDOFF" "Decisions + why" --text '<rulings>'(genuine judgment calls go throughcouncilfirst, per step 6). Use thesectionsubcommands rather than hand-splicing the markdown: ad-hoc regex edits to the one file the loop trusts are how it gets corrupted. - Resume the builder: send the ruling into its pane. If it finished the slice
but never ran
handoff.mjs ready, tell it to report and stop. - Relaunch the bridge (same command as after a dispatch) and end your turn.
If after step 1 there is genuinely nothing to rule on — no disagreement, no question, no prompt, pane mid-stream — relaunch the bridge once and say so in your status line. If the same signal fires again, surface it as a BLOCKER and ask the human. Never relaunch more than once without new evidence.
The wait bridge (push, not poll)
The builder already ends its run with handoff.mjs ready, flipping the handoff
to results-ready. The bridge turns that file-write into a harness wake-up so the
Claude side never polls. After each dispatch, run:
Bash(run_in_background: true):
H=$(node "<…>/offload/handoff.mjs" path "<the slice's worktree>" "$CLAUDE_CODE_SESSION_ID")
bash "<core>/complete/wait-for-ready.sh" "$H" builder
One bridge per dispatched slice, each on that slice's own handoff. Re-derive the
path here — shell variables do not survive the harness wake between dispatch and
this bridge. Use path <worktree> "$CLAUDE_CODE_SESSION_ID" for a slice in a
worktree, or resolve "$PWD" for one in the repo root; both refuse when
$CLAUDE_CODE_SESSION_ID is empty — surface that, don't guess a path. Resolve
<core> as the parent of the offload dir from the SessionStart [offload] line
(i.e. wait-for-ready.sh sits next to the offload dir under skills/core/). The
script blocks — via fswatch when available, else a cheap detached sleep-poll —
until one of its deterministic signals fires, then exits with a final WAITER:
line. All of that runs outside your context: zero tokens until it wakes you. The
signals, strongest first:
- the handoff flips to
results-ready(builder ranhandoff.mjs ready) orblocked(builder ranhandoff.mjs blockedon a mid-slice blocker); "$H.turn-ended"settles — dispatch wires the harness's own turn-end hook (codexnotify, claudeStop/Notification) to touch it whenever a builder turn ends. That hook fires on every sub-turn, so the bridge treats a fresh marker as a candidate stop and confirms it only afterWFR_TURN_SETTLEseconds (default 45) with no CPU growth and no session-log writes. A builder grinding through its gates no longer wakes you every few seconds; one that has genuinely stopped still wakes you in under a minute;- idle backstop: builder pid alive but no session-log activity (the
"$H.activity"sidecar dispatch writes) AND no CPU for the idle window; - liveness/timeout backstops: builder died, never started, handoff deleted, or the overall deadline passed.
Never rm the .turn-ended marker by hand. The bridge baselines it at
start; deleting it races with dispatch.sh and with a running bridge, and eats
real wakes. If a bridge is waking you too often, raise WFR_TURN_SETTLE — do
not disable the signal.
6. Field questions through council
When you hit a genuine judgment call — an arbitration ruling, a scope dispute, a
design fork — field it through council before deciding, rather than guessing.
The builder block already instructs the builder to resolve its own ambiguity via
the bork:council skill before coding (see the offload builder block).
7. Finish — merge-ready by default, merge only if authorized
Reached when every ledger row is terminal, every accepted slice's gates
passed and its per-slice review was clean, AND the goal text is fully delivered
by commits pushed to the branch. Run board and confirm that before anything
else: a non-terminal row means you are not finished, however complete it feels.
Final integration review — the readiness gate. Run
reviewon the FULL branch diff against the base; per-slice reviews can't see cross-slice integration issues. Treat the verdict as the gate:- SAFE TO LAND → ready.
- LAND WITH CAUTION → ready, but carry the caveats into the report.
- DO NOT LAND → not ready. Feed the blockers back as a corrective slice (return to step 5) and re-run this gate after it lands clean. Nothing is merge-ready until this gate is green.
A per-slice gate set proves only what it measured. Before accepting the last slice, check what the repo's own CI runs that your frozen gates did not — a green local suite next to a red pipeline is a failed slice, not a passed one.
Default — STOP at merge-ready. Do NOT merge. Report and hand to the human:
- goal, slices shipped, commit SHAs, final gate + review verdicts (and any
LAND WITH CAUTION caveats) — taken from
board, not from recollection; - the branch name and the exact merge command they can run. Leave the handoff and the ledger in place for the human's follow-up; do not end them.
- goal, slices shipped, commit SHAs, final gate + review verdicts (and any
LAND WITH CAUTION caveats) — taken from
Merge — ONLY if the goal explicitly authorized it (step 1). Then: confirm unless the user pre-authorized auto-merge in step 4; integrate via the repo's standard path (the builder already commits + pushes each slice, so this is the PR merge / fast-forward, not new code — prefer
ghfor a GitHub repo); set each merged slice--state merged; end the handoffs (handoff.mjs end "$HANDOFF"); report as in step 2 plus the merge result.
Hard rules
- You never write implementation code. If tempted, hand a tighter slice to offload.
- Every builder launch goes through
offloadanddispatch.sh. Never create one with rawherdr agent start/spawn,herdr pane split, tmux, Terminal, or a headless harness command. A herdr pane id without a tab id is a failed launch, not a dispatched slice. The ledger refuses a pane that did not come from dispatch.sh, andboardflags one that slipped in — treat either as a failed dispatch to redo, not an obstacle to override. - Run
boardat the start of every wake, and report only what it says. Slice state narrated from memory is the single largest source of wrong progress: it cannot see a builder that exited, and it does not survive a compaction. - Write terminal states as you decide them, in the same turn — including reversals. A slice with no terminal state reads as in-flight forever.
- Resolve every NO OWNER row before your turn ends. Either judge what the slice produced and mark it terminal, or dispatch a corrective. Diagnosing without dispatching leaves the slice with nobody on it.
- Never poll or ScheduleWakeup for the builder — the wait bridge wakes you. A short-poll ScheduleWakeup here is wasted work.
- Never hand-delete a
.turn-endedmarker or hand-splice a handoff's markdown. UseWFR_TURN_SETTLEand thesectionsubcommands. - Verdicts come from raw gate numbers vs frozen gates — never the builder's prose.
The builder's
## Work summaryis uncontrolled text that goes stale; it is for continuation, never for status. - Nothing is merge-ready until every slice passed
reviewand the final integrationreviewreturns no DO NOT LAND. The review gate is not optional or skippable. - Never edit frozen gates after results exist (offload enforces this; don't route around it).
- Default is stop-at-merge-ready. Never merge unless the goal text explicitly authorized it. When the instruction is ambiguous about merging, stop and hand to the human — do not merge.
- The handoffs and the ledger are session-scoped and never committed. Don't
git addthem. - Re-derive each handoff path via
resolve/pathon every wake — never carry a stale path or write to a handoff you don't own. An ownershiprefusing:error from the CLI is the wrong-document tripwire: STOP and surface it, never--stealpast it.