orchestrate · git:20260916.1e327a7 · 2026-09-16 · sha256 f9733a475dee6a57
orchestrate git:20260916.1e327a7A
Immutable. This exact content is served forever at /api/v1/blob/f9733a475dee6a57.
---
name: orchestrate
effort: high
argument-hint: "[goal or parent issue]"
description: Orchestrate one natural-language goal into parallel tmux Claude Code sessions. It decomposes the goal (with your approval), plans each task with wiki-plan, implements and reviews, runs an integration test, and merges after your confirmation. Use to build a goal across multiple sessions. For a single task, use loop-implement instead.
---
# orchestrate — multi-session orchestrator
You are the orchestrator. **You do not implement — sessions do.** You clarify,
decompose, distribute, review, integrate, and merge. Autonomy lives inside the
implementation loop; two human gates bracket it (task-split, pre-merge).
Scripts referenced below live in `${CLAUDE_PLUGIN_ROOT}/skills/orchestrate/scripts/`.
Communication: session→orchestrator via each worker's OWN worktree-local
`.orchestration/status/<task>.json` (never the coordinator's checkout — a
worker writing there can hit a guardrails permission prompt no human is
watching, issue #167), collected into the canonical `.orchestration/status/`
by `scripts/collect-status.sh` (Phase 3 step 1, and automatically each poll
via `watch-status.sh` — see **Session knobs**); orchestrator→session via
`launch-session.sh` (the first prompt) then `send-prompt.sh` (every later
one), carrying templates/session-prompt.md §1–§4 on tmux, or the Task `--spec`
(same file, §O1–§O4) on Orca.
## Asking the user — every question is a chooser (REQUIRED)
Every question this skill puts to the **user** is delivered with the
**AskUserQuestion** tool, so they answer by selecting an option instead of typing
a reply. Prose in the turn is the *briefing* (task list, graph, slot count, cost
note, diff); the *decision* is always the tool call. This covers Phase 0 frontier
rounds, Gate 1 (task split **and** substrate), Gate 2, a Phase 3 deadlock, and any
worker escalation that needs a human verdict.
**Required shape**
- One call per round, up to 4 questions in it. A frontier wider than 4 → back-to-back
calls of ≤4 questions, numbering continuous across them; never one question per turn.
- Each question carries 2–4 options; the **recommended answer is option 1**, its
label suffixed `(Recommended)`. `header` ≤12 chars.
- Options are the concrete outcomes ("approve as proposed", "drop task 3", "Orca"),
not a yes/no restatement of the prose.
- `multiSelect: true` when the items are independent (e.g. which optional tasks to include).
- Free-text is already available to the user as "Other" — never author an "Other" option.
**Predicate-keyed**
| Situation | Ask |
|---|---|
| Phase 0, frontier of open decisions | one question per frontier item, ≤4 per call, each with its recommended answer |
| Gate 1, `orca-detect.sh` non-zero (no Orca) | Q1 task split: approve as proposed / revise / abort. Say nothing about substrates |
| Gate 1, `orca-detect.sh` exit 0 (Orca present) | the SAME call carries Q1 task split **and** Q2 substrate: Orca / tmux |
| Gate 2, after showing the integration diff | Q1: merge / send back for rework / abort |
| Phase 3 deadlock, or an escalation needing a human | options = the concrete resolutions available, not "how should I proceed?" |
| You are a **worker** session (templates/session-prompt.md) | never AskUserQuestion — escalate via `ask-coordinator.sh`; only the coordinator asks the user |
**Pre-send check** — before ending any turn that asks the user something: does this
turn contain an AskUserQuestion call? Is the recommended option first and labelled?
If the turn instead ends with a question in prose, delete that prose question and
re-send it as the tool call. Enforcement is machine-side too: the
`orchestrate-ask-gate.sh` PreToolUse hook denies `launch-session.sh` /
`orca-worker-start.sh` / `orca-spawn.sh` until this session's transcript shows the
AskUserQuestion (and, when Orca is detected, one naming the substrate choice).
## Tool profile
Resolve the pluggable tool profile once up front:
`sh ${CLAUDE_PLUGIN_ROOT}/scripts/resolve-tools.sh --summary`. It maps capability
roles — `intake` (issue-tracker work-list source), `knowledge` (domain/policy),
`tacit` (incidents/danger zones), `verify` (test/build/QA
command), `explore` (code search; a fresh graphify graph when Preflight says
so), `design` (visual/UI spec, e.g. Figma) — to
whatever tools this installation has, or to generic defaults when unset (optional,
layered per-user then per-repo; see `references/tool-profile.md`). Use
`knowledge`/`tacit` yourself during Clarify/Decompose, and write each task's
resolved roles into its `<tools_guidance>` brief so worker sessions inherit them
even if they can't re-read the config. A role is a tool injected into one step,
never a loop: do not map a role to an implement/verify-loop tool or another
orchestrator (that nests loops); there is no `implement` role.
## Coordinator token budget
Accumulated context, not turn count, is what drives a run's cost — every call
resends the full history at the cached rate, so cost per turn grows as the run
goes on. Four things the coordinator itself controls:
(a) never `Read` a screenshot or other image directly into this session —
delegate the visual check to a subagent that returns a text verdict instead;
(b) bound every pane capture and status read (`tail -N` on tmux panes, one-line
`jq` filters on status JSON) instead of pulling full output into context;
(c) **say where a `/compact` is safe** — the coordinator cannot compact its own
session, so a human has to be told. The safe points differ in what they hand
back, so they are keyed by predicate:
| At this point | What to say | What gets re-read from disk afterwards |
|---|---|---|
| Gate 1, once the split is approved | a safe `/compact` point, if the run has already gone long | `.orchestration/graph.json` and the briefs |
| **After each merge-on-approval** — once `git merge-base --is-ancestor <branch> <integ>` has confirmed the merge landed (Phase 3 step 5) | a safe `/compact` point | `.orchestration/graph.json`, `.orchestration/status/*.json`, and the brief + plan of whatever `ready-set.sh` dispatches next |
| Gate 2, with the integration diff shown | a safe `/compact` point | the integration diff, re-read with `git diff` |
Gate 2's full-diff read is the LAST one of a run, not a recurring cost: Phase
5's integration review now runs on the `integration-reviewer` agent's own
fresh context, so this coordinator session itself never reads the full
integration diff until this final gate.
The middle row is the one that matters. Gate 1 and Gate 2 are the *first* and
*last* things a run does, so on their own they leave the long autonomous middle
— where essentially all of the context accumulates — with no sanctioned
checkpoint; one measured run peaked at **613,698** context, more than 2x
`token-report.sh`'s own warn threshold, and was told about it only after
teardown. That boundary is safe for exactly the reason **Re-entry** gives:
nothing unrecoverable is held there, because the graph, every task's phase and
`.attempt`, the briefs, the plans, the reviews and the merged commits are all
already on disk. It also recurs once per approved task rather than twice per
run.
(d) **measure it during the run, not only after it.** At that same
merge-on-approval boundary, before offering the `/compact`, read this
coordinator session's own peak context. Target the newest transcript in the
project's mapped directory, not the directory: the teardown form
(`--cwd <root> .`) reports every past session for this project, while the newest
transcript is this live session, since it is the only one being written during
the run.
```sh
d="$HOME/.claude/projects/$(printf '%s' "$PWD" | tr '/.' '-')"
newest=$(ls -t "$d"/*.jsonl | head -1)
sh {SKILL}/scripts/token-report.sh "$newest"
```
That prints a two-row table, plus one `warn: peak context …` line on stderr once
peak passes `LO_CTX_WARN` (default 300000). Pass the warn line to the user along
with the compact offer. The End-of-run contract runs the same script over the
whole run; this is that measurement taken while it can still change something.
**Known amplifier — the re-plan loop.** When a worker reports the plan is
contradictory or under-decided (Phase 3 step 2a), every round appends
permanently to coordinator context: the worker's gap report, your verification
of it against the code, the plan patch, and the re-send. One measured run did
eleven of them. Each round was worth doing — it caught a real defect every time
— so the fix is not to skip the loop but to keep the re-send short: the worker
re-reads `{ORCH_DIR}/plans/<task>.md` from disk anyway, so send `plan patched at
§N — re-read it from disk and re-run the adoption check` instead of restating
the fix in the prompt.
**Re-plan ladder — bound the loop.** Track each task's round count in your head
from the review/patch history already on disk (no new state file). Rounds 1–2
on a task are SCOPED PATCHES: the cheap re-send above, patching only the
reported gap. Round 3 is ONE full re-plan: rewrite `plans/<task>.md` wholesale,
re-running `wiki-plan` on the planning model rather than patching a single
section — a third scoped patch is the signal the section-level fixes aren't
converging. A task that still reports a plan gap after its round-3 full
re-plan is a deadlock-grade escalation to the user: report it and get a human
decision, same as a Phase 3 step 1 exit-3 DEADLOCK. There is never a round 4.
Basis:
`wiki/infrastructure/agent-orchestration/session-context-token-budget.md`
(directives 2–4), plus this skill's own **Re-entry** guarantee for the
merge-on-approval row.
## Preflight
Run `${CLAUDE_PLUGIN_ROOT}/hooks/preflight.sh` to resolve git/tmux/jq paths and
surface any missing CLI. **git, tmux, and jq are all required** — if any is
missing, stop and ask the user to install it (the SessionStart preflight hook is
advisory only; this skill must hard-require them). For a missing tmux: with the
user's consent, install it (macOS: `brew install tmux`; otherwise advise) before
launching sessions. Never auto-install without consent.
**Code-graph freshness (optional `explore` lead).** Run
`sh ${CLAUDE_PLUGIN_ROOT}/scripts/graph-freshness.sh <root>` once, bare (read
its exit code on the next line, never in a pipe). It never runs graphify
itself. Branch on the code: **0** (`fresh`) — set `explore` to graphify for
this run and write the row from Phase 3 step 2 into every brief; **2**
(`stale <N>`) — ask ONE chooser (§ Asking the user): "graphify graph is stale
(<N> files changed since it was built) — run `graphify update <root>` now
(AST-only, seconds, no LLM), or continue without the graph?" with *update* as
the recommended answer; run the update only on an explicit yes and re-run the
freshness check afterwards; **3**/**4** (`absent` / `cannot-evaluate <reason>`)
— print one line saying the graph is not in use and continue exactly as
before. The coordinator never runs a full `/graphify` build and never loads
the graphify skill document: the CLI is the whole interface
(`graphify --help`). Basis:
`wiki/infrastructure/agent-orchestration/code-graph-as-orientation-layer.md`.
**Coordinator permissions (tmux substrate).** `launch-session.sh` starts each
worker as `claude --permission-mode bypassPermissions` — the exact surface an
auto-mode permission classifier hard-flags as privilege escalation. It cannot
see the context that makes this safe (each worker worktree carries a guardrails
deny-net that still blocks dangerous commands in bypass mode and escalates
`ask` rules to you), so under an auto-mode coordinator the launch may be
DENIED. Handle this at onboarding, not at the first failure:
1. **Probe once, read-only**, during Preflight:
`sh {SKILL}/scripts/install-permission-rules.sh --check` — exit 0 rules
present (nothing to do), **4** absent, 3 the settings file is malformed
(surface that to the user; fix before anything else).
2. **On 4, ask the user — never install silently.** One question: "orchestrate's
tmux workers launch with permission prompts off (guardrails-sandboxed);
pre-approve the four worker-management scripts in ~/.claude/settings.json?"
Show what it adds (the snippet below) — this also covers the exit-5
escalation recovery path (`resolve-escalation.sh`), executable by an
auto-mode coordinator without any bare `rm`. A plugin that widens permissions
without a fresh explicit yes is the supply-chain pattern guardrails exists
to stop — and the fresh consent is also what lets the classifier pass the
write at all.
3. **On an explicit yes**, run `sh {SKILL}/scripts/install-permission-rules.sh`
(idempotent; backs up, refuses a malformed target, atomic write). On no —
or if the installer itself is classifier-blocked — fall back to showing the
snippet for the user to paste themselves, then continue; the launch will
simply prompt (default mode) or deny (auto mode) until it lands.
4. If a launch is DENIED later anyway: do **not** work around the block — an
agent widening its own permissions on its own initiative is itself
classifier-blocked, by design. Re-offer step 2 and stop until the user
decides.
What the installer adds (equivalently pasteable into
`.claude/settings.local.json` per-project, or `~/.claude/settings.json`
globally; `safe-cleanup.sh` is deliberately absent so destructive verbs keep
their normal review):
```json
{
"permissions": { "allow": [
"Bash(sh /Users/<you>/.claude/plugins/cache/*/dev-loop/*/skills/orchestrate/scripts/launch-session.sh *)",
"Bash(sh /Users/<you>/.claude/plugins/cache/*/dev-loop/*/skills/orchestrate/scripts/send-prompt.sh *)",
"Bash(sh /Users/<you>/.claude/plugins/cache/*/dev-loop/*/skills/orchestrate/scripts/watch-status.sh *)",
"Bash(sh /Users/<you>/.claude/plugins/cache/*/dev-loop/*/skills/orchestrate/scripts/resolve-escalation.sh *)"
]},
"autoMode": { "allow": [
"$defaults",
"Running the dev-loop orchestrate plugin's worker-management scripts (launch-session.sh, send-prompt.sh, watch-status.sh, resolve-escalation.sh) is allowed, including launch-session.sh starting a tmux worker with `claude --permission-mode bypassPermissions` and resolve-escalation.sh clearing a guardrails escalation record and delivering the coordinator's verdict to the waiting worker: the user sanctioned this orchestration workflow, and each worker worktree is sandboxed by groundwork guardrails, which still blocks dangerous commands in bypass mode and escalates `ask` rules to the coordinator. This does NOT extend to safe-cleanup.sh or other destructive commands, which keep their normal review."
]}
}
```
The path rules cover full-path invocations; the `autoMode.allow` rule teaches
the classifier the context so variable-form invocations (`sh $SKILL/scripts/…`)
pass too. The Orca substrate does not spawn through `launch-session.sh`, but
its worker terminals embed the same flag — if a classifier flags those, the
same `autoMode.allow` rule is the fix.
## Phase 0 — Intake + Clarify
Two ways the work-list arrives:
- **`intake` role configured** (e.g. an issue tracker) → if the user names a parent
issue (key/URL), use the intake tool to read it and its children: the parent gives
the overall goal/architecture, each child becomes a candidate task. Extract the
issue key from a URL (last path segment). This is the "Jira-style" entry — only
taken when `intake` is set *and* the user supplies an issue; otherwise:
- **`intake` unset, or a free-text goal** → the natural-language path (default):
decompose the goal yourself in Phase 2.
**Already-done children (partial resume):** treat a child as *completed* if the user
says so (by key) or — when intake exposes status — its tracker status is Done. A
completed child is **not** decomposed into a task (no session/worktree), but it is
**not silently dropped** either: record what it produced as a **base output** — the
exact exposed signature (function/type/component/endpoint). Any task that depends on
it then still gets that contract injected (Phase 2/3). If the user names a completed
key without the signature, read it from the integration branch / its merged code
rather than re-creating it. This is how "one sub-issue already done, do the rest"
works safely.
Either way, map open decisions as a **design tree** — each decision branches into
the decisions that hang off it — then run **frontier rounds**:
- A decision belongs in the **frontier** when its prerequisites are already
settled; a decision whose answer depends on another still-open decision waits
for a later round.
- A fact the environment can answer (repo state, config, docs) is the
orchestrator's job to look up — never asked of the user; only genuine
decisions go to the user.
- Each round, ask the WHOLE frontier at once — as **AskUserQuestion** questions
(§ Asking the user), one per frontier item, each a **question + a recommended
answer** (never a bare open question, never prose the user must type back).
Repeat rounds until the frontier is empty — goal / scope / constraints / done
criteria clear enough to decompose. Don't start until they are.
## Phase 1 — Environment branch
- **git repo present** → create a feature (integration) branch + one worktree per
task. Determine base via `gh repo view --json defaultBranchRef` (fallback: the
current branch) — measure, don't assume.
- **no git repo** → run `scripts/safe-cleanup.sh init-check <workdir>`. Only if it
returns ok, `git init` (add a `.gitignore` incl. `.orchestration/` first), then
proceed as above. If it REFUSEs (nested repo / secrets), stop and report.
- Create `.orchestration/notes/` and `touch` an empty `notes/decisions.md` —
the blackboard (append-only) lives there; see **Blackboard** below. Without
this, the first worker to reach the implement phase's read checkpoint reads
a file that does not exist yet.
## Phase 2 — Decompose
Get the task set: use the `intake` children as candidate tasks if Phase 0 read an
issue, otherwise split the goal into independent tasks yourself. **Drop the
completed children (Phase 0) from the task set** — but seed the dependency graph
with their base outputs (already-satisfied), so their dependents still resolve and
get the signature injected, while no session is spawned for them. Then, the same way
for both, for each *remaining* task extract **affected files**, **outputs** (what it newly
creates — component/schema/endpoint/type), and **consumes** (another task's output
it depends on). Build a conflict/dependency matrix from those and topologically sort
into Waves (`conflict-matrix.md`): a dependency edge `A → B` means B consumes A's
output, so A's Wave precedes B's. Detect duplicate outputs and assign a single
producer; others consume (add a dependency edge). **Mark shared surfaces** while
you do this: any output that lands in one task's `outputs` and at least one other
task's `consumes` is a shared surface — the conflict matrix already computes this
exact edge, so no extra pass is needed. Phase 3 step 0 commits a contract stub for
each one before its producer is dispatched (see Contract-first dispatch below).
Write BOTH artifacts: `conflict-matrix.md` for humans and
`.orchestration/graph.json` for the scheduler. A markdown table is not machine
readable.
**Graph leads (only when Preflight set `explore` to graphify).** For each
candidate task, run `graphify explain "<Symbol>" --graph
<root>/graphify-out/graph.json | head -40` on the symbols the task names, and
`graphify path "<A>" "<B>" --graph <root>/graphify-out/graph.json` for a
suspected edge between two tasks. The printed connections seed the task's
affected files and its shared surfaces; the disjointness test in the conflict
matrix runs on the confirmed sets, never on the graph's guesses. A graph hit is
a lead, not evidence: every file it suggests is confirmed by a search before it
enters `files`, and the record pairs both in one line —
`graphify explain <Symbol> -> <N> connections; grep -rn <Symbol> src -> <n> hits`.
Ask symbol-anchored questions only; a free-text `graphify query` is for
orientation (`--budget 800`) and never derives a file set. Any assumption taken
from the graph and not confirmed is recorded on the blackboard as
`graph-derived: <assumption>`.
```json
{ "tasks": [
{ "id": "t1", "deps": [], "files": ["src/auth/**"], "outputs": ["AuthToken"] },
{ "id": "t3", "deps": ["t1"], "files": ["src/api/**"], "consumes": ["AuthToken"] }
] }
```
Waves are **an illustration in the Gate 1 report, not an execution unit.**
Execution is decided by `ready-set.sh`: a task runs as soon as its dependencies
are `approved` and a slot is free. Still topologically sort — the result shows
the user the expected flow — but nothing waits on a Wave boundary.
**Propose the slot count.** Pick the number from the task count, their size, and
their risk, and **say what the number protects**: this cap guards **coordinator attention** and **API usage/budget**, not machine resources. Neither is
queryable, which is why it is a judgement rather than a computation. A slot is
held from dispatch until the task reaches a terminal state — `plan_ready` and
`impl_done` (review pending) count as held, because a pile of unreviewed tasks
next to a stream of new ones makes the cap meaningless. When `LO_MAX_SESSIONS`
is set it is an upper bound and overrides the proposal.
After writing it, run `scripts/ready-set.sh <graph> <status-dir> <cap>` once and
confirm it does **not** exit 4 — malformed JSON, a missing `.tasks` array, and a
dependency naming an undefined task are all caught here.
**Visual spec (`design` role).** While extracting the above, flag each task that is
UI-facing *and* whose source issue references a design (e.g. a Figma link). If the
`design` role is configured, pull that task's spec with it (don't guess from the
link) and carry it into the brief's `<design_spec>` (Phase 3 step 2). Backend-only
tasks, or any task with no design reference, skip this. With `design` unset, ignore
design links entirely — the original behavior.
## 🚦 Gate 1 — task-split approval (REQUIRED)
Report the task list, the dependency graph (showing the expected flow as Waves is
fine), the proposed **slot count with its rationale and what it protects**, and a
rough cost note. That report is the briefing; the approval itself is **one
AskUserQuestion call** (§ Asking the user) in the same turn — Q1 the task split
(approve as proposed / revise / abort). Bundle every remaining open decision
(task-set options, substrate choice) into that same call as further questions,
each with a recommended answer — never a separate turn per decision, never a
prose question. **Wait for the user's approval** before launching anything.
**Substrate — ask here, in this same turn.** Before writing that report, run
`scripts/orca-detect.sh`. Non-zero (no Orca): tmux, silently — say nothing about
substrates. Exit 0: report that Orca was detected and carry the choice as a
**question in the same AskUserQuestion call** as the split approval, options
— **Orca**: native trust-screen handling, event-driven waits, native
liveness; **tmux**: mid-flight steering via `send-prompt.sh`, no extra dependency. **Wait
for the user's answer**; their answer decides, and you carry it into Phase 3. A
detected Orca always asks — there is no default, no remembered choice, no
environment override. Launch nothing until both the split and the substrate are
answered.
## Phase 3 — Launch + plan (dispatch loop)
**Phases 3–4 are one dispatch loop, not a per-Wave repeat.** Each round:
1. First `scripts/collect-status.sh .orchestration/graph.json .orchestration/status
.worktrees` (pulls in any worker-local record `watch-status.sh` has not yet
polled since the last round — `ready-set.sh` reads the status dir directly, it
does not poll). Then `scripts/ready-set.sh .orchestration/graph.json .orchestration/status <cap>`
→ **0** dispatch the printed ids, **2** nothing dispatchable but work is in
flight (go wait for an event), **3** **DEADLOCK** — a failed dependency or a
cycle: **do not wait**, report it and get a human decision (with no worker
running, no event can ever arrive); after the human intervenes, return to
step 1 to re-run the check, **4** the graph or status could not be read —
refuse, do not guess; fix the error then re-run step 1, **5** every task is
in a terminal state → go to Phase 5.
2. For each dispatched task (`<N>` = the number of tasks in this round):
- tmux: **0** (Preceding-interface injection) + steps **1–3** below (setup,
brief, launch, watch plan_ready). Orca: **O1–O5**.
- **1** `scripts/setup-worktrees.sh <integ> <root> <base> <branch>...` then
verify with `git worktree list`.
- **2** Per task: write `briefs/<task>.md` (templates/brief.md) — fill
`<tools_guidance>` and `<design_spec>` — then launch session and watch
until `plan_ready` (step 3 below). **Write the brief at dispatch time.**
It only needs the signatures this task consumes, and by then those are
`approved`, so they're settled. When `explore` is graphify, the
`<tools_guidance>` row is: `explore: graphify — graphify explain
"<Symbol>" --graph <main-root>/graphify-out/graph.json | head -40 (lead,
not evidence; the graph reflects the integration base, and a worktree
carries no graphify-out)` with `<main-root>` the absolute main-checkout
path.
- **3** Collect `plans/<task>.md` when each session reaches `plan_ready`.
3. For each planned task, deliver §2 (implement) with `scripts/send-prompt.sh
send lo-<n> "<prompt>"` (tmux, see Phase 4 for exit-code branch logic), or
`orca orchestration task-create` the implement Task then
`scripts/orca-worker-start --task <impl_task> --terminal <handle> --worktree
id:<repoId>::<path>` (Orca; a bare `--terminal` with no `--worktree` is
rejected — `terminal_worktree_mismatch`, see O3).
On delivery failure, re-run step 3 after fixing the error.
4. Wait for event. tmux: `scripts/watch-status.sh --tasks <running ids>
<status-dir> impl_done <N>` — without `--tasks` the tasks approved in
earlier rounds satisfy `expected=<N>` immediately and the wait spins. Orca:
`scripts/orca-wait.sh` with the implement Task ids, already event-driven.
5. On wake, handle that task: review each worktree diff (`git -C <wt> diff
<integ>...HEAD`). If tests weak, audit with `test-quality-auditor`. **On
approval, merge before you loop (issue #90):** fast-forward first —
`git fetch . <branch>:<integ>` (no checkout needed, works while the main
worktree sits on another branch) — and only fall back to
`scripts/safe-cleanup.sh merge <root> <integ> <branch>` when that is not
fast-forwardable (that verb DOES check out `<integ>` in the main worktree).
Either way, verify the merge landed with `git merge-base --is-ancestor
<branch> <integ>` before dispatching any dependent — that exit code is the
evidence, not the merge command's own chatter. Then append one line to the
blackboard with the shell append primitive, never Write/Edit — see
**Blackboard** below: `printf '%s\n' '- [<task>] merged: <outputs> now on
<integ>' >> .orchestration/notes/decisions.md`. Then return to step 1:
whatever dependency it released shows up in the next
`ready-set.sh` round already merged, and the freed slot refills immediately.
On rework needed, run
the Phase 4 rework sequence — `status-update.sh <task> rework`, read the
new `.attempt`, write `reviews/<task>-rN.md`, re-deliver with
`send-prompt.sh send` (or a new Orca Task on the same terminal). The
rework budget is `ready-set.sh`'s job (`LO_MAX_REWORK`); exhaustion
surfaces as exit-3 DEADLOCK, handled at step 1.
When `ready-set.sh` returns **5**, go to Phase 5.
**Session knobs (tmux substrate, set once per run):** `export LO_RUN_ID=<short-run-id>`
so every `launch-session.sh` gets a collision-proof name `lo-<n>-<run-id>` (reuse that
exact name for later `send-prompt.sh`); the script also exports the guardrails
escalation env into each worker. Also export `LO_GRAPH=.orchestration/graph.json
LO_WORKTREES_ROOT=<root>/.worktrees` once per run: with both set, every
`watch-status.sh` call pulls each worker's worktree-local `.orchestration/status`
(and `questions/`) records into the canonical dir automatically, once per poll
(issue #167 — workers never write into this checkout). It also collects
`blocked/` records (the `worker-blocked-signal.sh` hook) the same way, except a
canonical `blocked/<task>.json` is pruned once its worker-side copy is gone —
see exit 8 below. `ready-set.sh` reads the
status dir directly and does not poll, so before every `ready-set.sh` round also
run `scripts/collect-status.sh .orchestration/graph.json .orchestration/status
.worktrees` yourself (Phase 3 step 1). Trust-screen wording drifts between CLI releases —
if a launch hangs, set `LO_READY_EXTRA` / `LO_TRUST_EXTRA` (substrings) or
`LO_READY_TIMEOUT`. `LO_PASTED_TAIL_LINES` (default 40) is `send-prompt.sh`'s
fallback pasted-marker window, scanned only when it cannot locate the input box
in the pane capture — raise it if a CLI release changes the input-box chrome so
the box stops being locatable. `status-update.sh` resolves the status file's `session` field
from `tmux display-message -p '#S'` only when the caller is itself inside tmux
(`$TMUX` set) — a coordinator-side call (this shell, not a worker's tmux pane)
must pass `STATUS_SESSION=<lo-n-runid>` explicitly, or the record's `session`
field is left absent rather than guessed from whatever tmux session happens to
be active.
`watch-status.sh` now exits **5** on a pending guardrails escalation, one line per
record naming its `recorded <ts>` next to the task's current `phase @<updatedAt>` —
triage from that line alone: if the phase transition is LATER than the record, the
worker already moved on by bypassing the denial, so `rm` the record WITHOUT
delivering an answer (nothing is waiting for it). Otherwise the worker is still
waiting: approve/deny, then `sh {SKILL}/scripts/resolve-escalation.sh <escdir>
<task> lo-<n> "approved — re-run: <cmd>, then continue"` (or `"denied —
<alternative>"`) — it clears the record(s) and delivers in one pre-approved
call; the guardrails deny message told the worker the orchestrator would
re-run it, so a cleared escalation without a delivered answer leaves it
waiting forever. On exit 6, retry delivery directly with
`scripts/send-prompt.sh send lo-<n> "<same message>"` — then relaunch. Exits **3** on a failed
OR a *dead* worker (a non-terminal task whose tmux session vanished — recorded via
the status file's `session` field) — both abort fast instead of waiting the
timeout. It also exits **6** on a pending worker question, **7** on a live worker
whose pane is stalled, and **8** on a blocked worker (a
`.orchestration/blocked/<task>.json` record from the
`worker-blocked-signal.sh` hook, confirmed by a pane that stayed unchanged
across two polls) — playbooks in Phase 3 step 3. It presses Enter itself
when a worker's input box holds an unsubmitted paste on two consecutive
polls, unless `LO_AUTO_RECOVER` is `0`, `off`, or `false`;
`LO_AUTO_RECOVER_MAX` (default 3) bounds those presses per session per watch
process, and a malformed value is refused with **exit 4**. Give each
phase its own deadline with one exported
`LO_PHASE_TIMEOUTS="plan_ready=900,impl_done=3600,done=1800"`, keyed on the TARGET
phase of each wait: precedence is an explicit `[timeout-sec]` argument, then the
matching entry, then the 3600s default. A malformed entry (no `=`, non-numeric,
`<= 0`, or an unknown phase name) is refused with **exit 4**, never silently
defaulted; the effective budget and its source are printed before the wait.
`LO_MAX_REWORK` (default 3) is the rework budget `ready-set.sh` enforces —
same validation as `LO_MAX_SESSIONS` (empty/non-numeric/`<= 0` refused with
**exit 4**).
**The coordinator shell may be zsh:** zsh (the macOS default) does not
word-split unquoted parameter expansions the way bash does, so a bash-style
loop such as `for pair in "$name $task"; do launch-session.sh $pair; done`
silently passes the whole `"name task"` string as one argument instead of
splitting it into two. Launch each task with its own explicit
`launch-session.sh` call, arguments spelled out per task, never through a
splitting loop. `launch-session.sh` now fails fast against exactly this class
of bug: it exits **2** before any tmux call when the final session name
(after the `LO_RUN_ID` suffix) is empty or contains a character outside
`[A-Za-z0-9_-]`, when the worktree argument is not a git work-tree root, or
when exactly one of `LO_STATUS_DIR`/`LO_TASK_ID` is set (set-but-empty counts
as unset). A malformed caller now gets an immediate exit 2 instead of a
silently launched broken worker.
**Substrate (the user decided at Gate 1 — do not re-decide here):** the answer was
Orca or tmux. **Orca** → spawn **and supervise** workers through it, not raw tmux:
it resolves the trust/TUI screen, gives native liveness, and pushes worker events to
you instead of making you poll. Replace steps 1–3 below with O1–O5:
- **O1 — bind the Run once per orchestration run.**
`orca orchestration run-create --objective "<goal>" --json` → keep `run_id` for the
whole run (on re-entry, `orca orchestration run-use --id <run_id> --json` instead).
- **O2 — one Task per task-*phase*.**
`orca orchestration task-create --spec "<the prompt>" --task-title "<short>" --json`
→ `task_id`. Use `templates/session-prompt.md` §O1–§O4 as the `--spec` body; the
tmux §1–§4 one-liners are for `send-keys` and say "wait", which is wrong here.
A `worker_done` settles a Task exactly once, so plan / implement / rework are
separate Tasks. **You sequence them yourself** — dispatch phase N+1 only after
phase N reads `completed`. Do *not* chain with `--deps`: it takes a JSON array
(`--deps '["task_x"]'`; a bare id errors), and even then a task created that way
came back `task_not_startable` with its dependency already `completed`
(isolated: an identical task with no deps started fine on the same terminal).
- **O3 — start the worker.** First make sure the integration branch exists in the
main repo (`git branch <integ> <base>` if it does not) — the tmux path gets this
from `setup-worktrees.sh` step 1, this path does not. Then create the Orca
worktree **from that branch**: `orca worktree create --repo id:<repoId>
--name <task> --base-branch <integ> --no-parent --setup run --json` → copy the
whole `worktree.id`. `--base-branch` is **not optional here**: omit it and Orca
branches from the repo default (its documented fallback), so a Wave-2 task would
not build on Wave 1's merged output and Phase 6's merge would drag in unrelated
default-branch drift. Then scope guardrails inside it — **required, not a
nicety** — with `scripts/worker-guardrails.sh <worktree-path>`, the same
single-source script `setup-worktrees.sh` calls on the tmux path. Skip it and
the worker inherits the *repo/global* rules instead of the sandbox ones, so
routine in-worktree work (`rm -rf ./build`, `git checkout .`) turns into a
`deny` + escalation and the worker stalls on the coordinator for every one of
them. Then, with the escalation env exported:
`GROUNDWORK_ESCALATION_DIR=<abs> GROUNDWORK_TASK_ID=<task>
scripts/orca-worker-start.sh --task <task_id> --worktree id:<repoId>::<path>
--agent claude` → prints `dispatch=<id>` and `handle=<agent-handle>`. For this
task's **next** phase pass `--terminal <handle>` **together with** `--worktree
id:<repoId>::<path>` instead, so the session keeps its context — a bare
`--terminal` with no `--worktree` is rejected with `terminal_worktree_mismatch`,
because Orca resolves a bare terminal handle against the *caller's* own
checkout, not the worker's (measured: run_08cb6f65cbfa, 2026-08-29, and one
prior run). `worker-start --agent` alone cannot carry environment variables, so with
the escalation env set the script creates the agent terminal itself
(`terminal create --command`, the escalation contract + `--permission-mode`) and
binds the Dispatch to it; that is why the worktree must exist first and why
`new-child` / `new-top-level` are refused in this mode.
Bare `worktree create` leaves one unused fallback shell beside the agent
(measured: 3 workers → 4 terminals). After the worker is up, confirm with
`orca terminal list --worktree id:<...> --json` that the extra handle is an idle
shell — not a configured default tab — and close just that one with
`orca terminal close --terminal <handle> --json`.
A `worker-start` that fails **spends the Task**: it goes to `status=failed` and
every later attempt on it returns `task_not_startable`. Do not retry the same
Task — create a fresh one with the same spec. (Seen with `runtime_unavailable`,
which is what you get when that terminal is still busy with another Dispatch.)
The **not-spent exception**: `terminal_worktree_mismatch` (above) fires
*before* dispatch — Orca rejects the bad `--terminal`/`--worktree` pair before
ever handing the Task to a worker — so the Task stays `ready`, not `failed`,
and MAY be retried on the same Task with corrected args. The spent rule
applies to failures at or after dispatch (e.g. `runtime_unavailable`).
- **O4 — wait on pushed mail, not on a timer.** Workers on this substrate also
write status worker-locally (issue #167) — Orca's own mailbox is not that
channel, so after EVERY `orca-wait.sh` return, before acting on the events,
run `scripts/collect-status.sh .orchestration/graph.json .orchestration/status
.worktrees` to pull those records into the canonical dir.
`GROUNDWORK_ESCALATION_DIR=<abs> scripts/orca-wait.sh [--until-all] <timeout-ms>
[<task_id,task_id,...>]` → **0** completions arrived (acked — process them),
**2** window elapsed *or* the ack did not land (checkpoint, just re-run), **3** a
worker reported failure, **4** the runtime itself did not answer (`ok:false`, a
nonzero status, or a connection lost mid-wait) — an **outage, not a checkpoint**:
run `orca status --json` before waiting again, and restart nothing on this code
alone, because a dead runtime does not stop a worker session (measured: workers
kept committing and pushing while the runtime was down), **5** escalation
pending (approve/deny, then **`sh {SKILL}/scripts/resolve-escalation.sh
.orchestration/escalations <task>`** — no delivery args on the Orca path,
since answers go through orca, not send-prompt.sh; use the 2-arg form — then
re-run — like watch-status, code 5 recurs while a record is still on disk,
by design), **6** question pending
(`orca orchestration reply --id <msg_id> --body "<answer>" --json`, re-run).
**Always pass this Wave's task ids — that is correctness, not a progress
nicety.** Heartbeats reach this mailbox despite `--types`, and so do completions
from an earlier Wave or an unrelated Task; without the ids none of that can be
told apart from your own work. With them, exit 0 means a *successful
`worker_done` for one of your ids* and the `completed=<c>/<n>` line is scoped to
this Wave. Add `--until-all` to keep consuming batches until every listed id is
completed, so one Wave costs one coordinator turn instead of one per batch; it
still returns immediately on 3/4/5/6. Codes 3/5/6 leave the batch unread on
purpose, so an unhandled event is never silently dropped — which also means
delivery is **at-least-once**: a replayed batch must be processed idempotently
(key off `taskId`, never off a local counter). `ORCA_WAIT_RECHECK_MS` (default
15000) slices the wait so a guardrails record written *while* you are blocked
surfaces within one interval instead of one full `<timeout-ms>`. When polling
ANY inbox on this substrate, diff the SET of message ids you have already
processed, never the count — a new message can arrive in the same interval
another one leaves, leaving the count unchanged while the content changed.
- **O5 — liveness, in two questions.** `scripts/orca-worktree-alive.sh <wt>`
(0 alive / 1 dead / 2 unknown — treat unknown as *not* dead) replaces watch's
tmux check. It only asks whether a terminal is attached, which a wedged worker
passes: measured, three workers sat on an interactive CLI prompt for 75 minutes,
ALIVE the whole time, Tasks still `dispatched`, no escalation, diffs unchanged.
So also run `scripts/orca-worker-stalled.sh <wt>` (0 progressing / 1 stalled /
2 unknown — treat unknown as *not* stalled; `ORCA_STALL_MS`, default 600000).
A stall is not a failure to act on blindly: read the worker's screen
(`orca orchestration worker-read --dispatch <id> --limit 40 --json`) before you
decide, because "wedged on a prompt" and "finished but never reported" look the
same from the outside and need opposite responses. When the screen shows the
worker parked on a permission dialog, `orca terminal send --terminal <handle>
--text "2" --enter` clears it — decline (2) is the safe answer; granting the
permission on the user's behalf is not something the coordinator does
unattended.
**You cannot steer a running worker.** `orchestration send --to dispatch:<id>`
lands in the worker's mailbox, which a Claude worker never polls, and a new
Dispatch cannot be delivered while the current one runs (that is O2's
`runtime_unavailable`). So a mid-flight correction does not arrive: the supported
path is the Phase-4 review/rework round. (If you do send one, omit `--type` —
`note`/`guidance`/`message`/`info` are all rejected as `invalid_argument`.)
**Worker protocol on this substrate — put this in every `--spec` you dispatch.**
The worker still calls `status-update.sh` at each phase (the status files remain the
durable re-entry state); on top of that it must:
1. report the phase exactly once — `orca orchestration send --type worker_done
--subject "<status>" --body "<what changed, what remains>" --task-id <task_id>
--dispatch-id <dispatch_id> --outcome succeeded|failed --files-modified "a,b" --json`
(a failure is `--outcome failed`, never failure encoded only in prose);
2. forward a guardrails escalation instead of stalling on it — when a command is
denied with an escalation notice, `orca orchestration send --type escalation
--subject "guardrails <rule>" --body "<command + why>" --task-id <task_id>
--dispatch-id <dispatch_id> --json`;
3. use `orca orchestration ask --question "<q>" --timeout-ms <n> --json` for a
blocking question, and then end its turn — and if that window expires, resume the
same question with `ask --resume <message_id>` rather than deciding it or asking
it again. A timeout leaves the question pending; it is not an answer. Measured on
a 3-worker run: at 600s and 900s both workers instead "proceeded on a conservative
assumption" and reported the guess after the fact.
Step 2 is the *fast* path for a guardrails block — it arrives with the worker's own
context. It is not the only one: `orca-wait.sh` pre-checks
`GROUNDWORK_ESCALATION_DIR` before it blocks, so a guardrails record still surfaces
when the worker never sends the message (it died, or it is not a Claude session).
Export that dir for both scripts and the file stays the safety net the tmux path
already relied on.
If the user chose tmux at Gate 1 (or `orca-detect.sh` was non-zero), use the tmux
`launch-session.sh` + `watch-status.sh` path below, unchanged. Always verify each
Orca `--json` result before relying on its fields (`worker-start` returns
`.result.dispatchId`, and the agent handle as the `role:"agent"` entry in
`.result.effects[]`). `orca-spawn.sh`
remains only for a worker needing custom agent argv (e.g. codex `--model` /
reasoning-effort flags) that `worker-start` cannot express.
0. **Preceding-interface injection (Wave 2+, and completed base outputs):** before
launching this Wave, fill each task's brief `<dependencies>` with the **exact
signatures** of (a) the approved preceding Wave and (b) any completed (partial-
resume) child it depends on — the real signature, not a paraphrase. This is the
contract the downstream session plans against; loose text invites drift. Wave 1
with no completed dependencies skips this.
**What a `deps` edge delivers, and what it does not (issue #90).**
`graph.json` `deps` gate DISPATCH on the dependency's approval, and this step
injects its SIGNATURE — never its merged CODE. Merged code reaches a task's
worktree only through the merge-on-approval rule (step 5 below): a new
worktree branches from the integration branch's tip at the moment
`setup-worktrees.sh` runs (step 1), so it contains a dependency's code only
if that dependency was already approved and merged by then. A task that must
read merged results directly — realigning docs against another task's actual
diff, asserting an invariant that spans two tasks' code — cannot rely on the
signature alone: give it `deps` on every producer it reads, so it is not
dispatched until each producer is approved and merged.
**Contract-first dispatch (shared surfaces) — ordering exception.** A task
that PRODUCES a shared surface (Phase 2) reorders its own steps so the stub
it commits can quote a real signature and land before its own worktree
exists: run step 2a's `wiki-plan` invocation for this task FIRST — write
`plans/<task>.md`, but do not launch yet — then commit the stub below, THEN
step 1 (`setup-worktrees.sh`, whose worktree now branches from a tip that
already contains the stub), then step 2 (write the brief, referencing the
already-written plan) and launch. A consumer task needs no reordering of its
own: by round/dependency ordering its step 1 always runs after the
producer's stub commit has landed, so its worktree inherits the contract
normally.
Commit the stub in a temp integ worktree: `git worktree add
.worktrees/integ-stubs <integ>`, write the stub file(s) at the plan-named
repo-relative paths — the exact signature `plans/<task>.md` step 2a just
decided, plus a `// contract: <task> owns the implementation` marker comment
(in the stub language's comment syntax), never implementation — commit
`chore(orchestrate): contract stubs for <task>`, then `git worktree remove
.worktrees/integ-stubs`. The producer's brief says it IMPLEMENTS the stub in
place, not create it fresh. A producer that must change a committed stub's
signature reports that as a plan gap: you re-decide, recommit the stub, and
notify every consumer via the blackboard (append-only, shell `>>` primitive
— see **Blackboard** below).
1. `scripts/setup-worktrees.sh <integ> <root> <base> <branch>...` — each worktree
is created AT DISPATCH TIME, branched from the integration branch's CURRENT
tip; never pre-create a worktree for a future wave, or it misses whatever
merges land between now and that wave's dispatch. Then verify with `git
worktree list` — the script's own `base=<hash>` line on each new worktree
names what it actually branched from.
2. Per task: write `briefs/<task>.md` (templates/brief.md) — fill `<tools_guidance>`
from the resolved tool profile so the session uses the right knowledge/tacit
tools (the plan step is fixed to `wiki-plan`, not a configurable role), and for
a UI-facing task fill `<design_spec>` with the `design` role's pulled spec
(Phase 2) — then
The brief and plan are what the worker reads, not what it writes, so every
reference to them inside a composed prompt uses the `{ORCH_DIR}` token
(absolute path to this run's `.orchestration` dir, substituted like every
other `{...}` token) — the worker's cwd is its own worktree, which does not
contain `.orchestration/`. Status/questions are the opposite case: a worker
writes those worktree-locally via the RELATIVE `STATUS_DIR=.orchestration/status`
(issue #167), never through `{ORCH_DIR}` — the coordinator's own checkout is
never a target a worker writes to. Repo files (source, tests, tracked docs) stay
relative to that cwd instead: an absolute repo path would make the worker
edit the main worktree rather than its own. This coordinator's own
`briefs/<task>.md` / `plans/<task>.md` references above stay relative — the
coordinator's cwd is the main repo root.
**2a. Plan it yourself, here, before launching.** Invoke the bundled `wiki-plan`
skill for this task and write the result to `plans/<task>.md`. Planning runs in
THIS coordinator session on purpose: a worker can be pinned to a cheaper tier
(`DEV_LOOP_WORKER_MODEL`), and a plan is where an unmade decision becomes the
implementer's guess — so the plan must come from the strongest model in the run,
not from whatever tier is executing. Every design decision must be made and
grounded in a `wiki/` page (record the decision->page map); leave nothing "as
appropriate". The worker then ADOPTS this plan (session-prompt §1 / O1) instead
of authoring one, and still signals `plan_ready` — so the phase sequence, the
`plan_ready` watch, and the ready-set scheduler are all unchanged.
**Confirm gate evidence before dispatching.** If this task's `wiki-plan`
invocation ran Phase A/B, `.dev-loop/gates/plan-A-<task>.md` and
`plan-B-<task>.md` must exist and `gate-check.sh --run` must exit 0 against
each — a lite-mode `ABANDON` entry with a recorded reason still counts as
passing that gate. A plan with no evidence — the ledger is missing, or any
gate reads `UNMET` or `CLAIMED` — must not be dispatched: send the task
back to `wiki-plan`'s corresponding Phase instead of launching a worker on it.
**Read the Size verdict before launching.** `plans/<task>.md` step 5 carries a
REQUIRED `## Size verdict`; read it now, before `launch-session.sh` — the task
is still undispatched at this moment, so no status file exists for it yet.
`small`/`medium` — continue below, unchanged. `large` — do NOT launch this
task. Instead attempt `scripts/graph-drop.sh .orchestration/graph.json
.orchestration/status <task-id>` on the oversized node and branch on its exit
code: **exit 0** (no dependents) → add each piece the verdict recommends as an
independent node with `scripts/graph-add.sh .orchestration/graph.json
'<node-json>'` — `split_of` naming the dropped parent id, `deps` the parent's
former deps, `outputs` partitioned among the pieces — real parallelism, since
each piece now competes for its own slot; **exit 3** (dependents/consumers
still name it) → do not force the drop — keep the original node and fall back
to the existing overlap-split semantics (**Splitting a task mid-run** below)
instead. Either branch, report the changed task list to the user immediately
— same duty as a mid-run split — then resume this loop with the (possibly
changed) task set.
Because planning happens here, **the planning model is whatever model this
coordinator session is running**. There is no separate setting to turn: to plan
on a stronger tier than you implement on, start the coordinator on that tier
(`claude --model <planning model>`) and leave `DEV_LOOP_WORKER_MODEL` pointed at
the cheaper implementer tier.
A worker that reports the plan is contradictory or under-decided is telling you
the planning pass was wrong: fix `plans/<task>.md` here and re-send §1. Do not
let the worker re-plan — that silently moves planning back onto the worker tier,
which is the thing this step exists to prevent. Then
`LO_STATUS_DIR=<abs status dir> LO_TASK_ID=<task> scripts/launch-session.sh
lo-<n> <worktree> bypassPermissions "<plan prompt>"`
(plan prompt = templates/session-prompt.md §1 — the tmux set — with the
subagent + tmux worker protocol blocks). With BOTH vars set, a successful
launch (the confirmed-submission path AND the session-reuse path) pre-seeds
`<LO_STATUS_DIR>/<LO_TASK_ID>.json` phase=pending with the resolved session
name via the sibling status-update.sh, so dead/stalled-worker detection covers
the pre-plan_ready window; a seeding failure warns on stderr only and never
changes the exit code; either var unset = the previous behavior exactly.
Exit **0** = launched *and* the prompt confirmed
submitted; **4** = the REPL never became ready (relaunch); **5** = the prompt was
sent but submission could NOT be confirmed — the session is alive and may be
holding an unsubmitted prompt, so read it with `scripts/send-prompt.sh state
lo-<n>` and branch on its exit: **9** (`unsubmitted`) means the prompt is
already in the box — `scripts/send-prompt.sh keys lo-<n> Enter`, never a
re-send (that double-pastes); re-send only when `state` reports `ready`/0.
Never launch a second session on top of it.
3. `scripts/watch-status.sh <status-dir> plan_ready <N>` in the background; when it
exits, collect `plans/<task>.md`. *(Orca substrate: `scripts/orca-wait.sh
<timeout-ms> <this Wave's task ids>` per O4 instead — same exit-code contract,
event-driven.)*
*(Plans proceed autonomously per the user's choice — no per-plan gate.)*
`watch-status.sh` only answers "does the session still exist"; a worker can hold a
live session and produce nothing for hours. So on a long wait also run
`scripts/tmux-worker-stalled.sh lo-<n>` (**0** progressing / **1** stalled /
**2** cannot tell — treat unknown as *not* stalled; silence threshold
`LO_STALL_SEC`, default 600s), the tmux mirror of O5. Read the pane before acting
on a stall: "wedged on a prompt" and "finished but never reported" look identical
from outside and need opposite responses.
**Watch exit playbooks (tmux).** Mechanical responses for the non-terminal watch
exits — handle, then relaunch watch with the same target:
- **6 — question pending** (prints `[watch] question pending — <task>: <question>`;
recurs while `questions/<task>.json` exists, like exit 5; exit 5 wins when both
are pending): read the record (`{ts, taskId, question, options, worktree}`),
answer with `scripts/send-prompt.sh send lo-<n> "<answer>"`, then delete BOTH
the canonical `.orchestration/questions/<task>.json` AND the worker-side copy
at `<record.worktree>/.orchestration/questions/<task>.json` — the collector
only copies a question record when the canonical one is absent (copy-if-
absent), so leaving the worker-side file behind re-collects the
already-answered question on the very next poll. If the task's status was recorded `phase=failed` when it asked the
question, reset it to the phase you actually observe (read the worker pane
first) BEFORE relaunching watch — the reset IS a normal status write, not a
new phase word: `STATUS_DIR=<dir> STATUS_SESSION=<lo-n-runid> sh
scripts/status-update.sh <task> <observed-phase> note="reset after exit-6
answer"` — otherwise `watch-status.sh` counts the stale `failed` phase and
aborts with exit 3 again on the very next poll. Then relaunch watch.
- **8 — worker blocked** (prints `[watch] worker blocked — <task>:<session>
(<reason>: <detail>)`; recurs while the canonical record exists and the
pane stays static): act on `<reason>` — `rate_limit` / `overloaded` /
`server_error` → read the pane, THEN delete BOTH
`.orchestration/blocked/<task>.json` and
`<record.worktree>/.orchestration/blocked/<task>.json` right away (the
collector copies a worker-side record back otherwise; deleting only after
a multi-hour wait means the same static pane re-wakes exit 8 on every
poll — about every 30s — for the whole window); THEN, if the CLI shows
automatic continue armed (`autoContinueAtUsageLimit`, on by default for
interactive claude.ai-subscription sessions when the reset is under 24
hours), just wait and relaunch watch once it resumes; otherwise follow the
usage-limit branch of 7 below and relaunch watch only after that wait;
`quota_auto_resume_stale` → the pane asks to press enter to continue:
`scripts/send-prompt.sh keys <session> Enter`; `quota_auto_resume_disabled`
→ automatic continue will not resume the task (turned off, reset more than
24 hours out, continuation dropped, or retry cap exhausted): report it to
the user; `permission_prompt` / `elicitation_dialog` /
`elicitation_url_dialog` / `agent_needs_input` / `worker_permission_prompt`
→ read the pane and answer with `scripts/send-prompt.sh keys` (decline is
the safe answer); `idle_prompt` → finished-but-silent or a question asked
in prose: send a prompt to emit the missing signal or to use
`ask-coordinator.sh`; `authentication_failed` / `billing_error` / anything
else → report to the user. For every reason OTHER than `rate_limit` /
`overloaded` / `server_error` (handled above, before its wait), then
delete BOTH `.orchestration/blocked/<task>.json` and
`<record.worktree>/.orchestration/blocked/<task>.json` (the collector
copies a worker-side record back otherwise) and relaunch watch.
- **7 — stalled live worker** (prints `[watch] worker stalled — <task>:<session>`;
the weakest signal — failed(3) and all-reached(0) win over it; driven by
`tmux-worker-stalled.sh`, silence threshold `LO_STALL_SEC` default 600s; a
missing script or tmux disables the check): read the pane FIRST
(`tmux capture-pane -t "=<session>:" -p | tail`), classify with
`scripts/send-prompt.sh state lo-<n>`, then act — **9** (`unsubmitted`, the
input box holds a parked paste) → `scripts/send-prompt.sh keys lo-<n> Enter`
(watch now presses this Enter itself on two consecutive observations unless
`LO_AUTO_RECOVER` disables it, so reaching 7 with 9 means auto-recover is off
or capped),
then relaunch watch (this is the exact path a field incident took ~10 minutes
to find via the stall timeout instead); interactive chooser → answer with
`scripts/send-prompt.sh keys <session>
<key>...` (allowlist exactly `Up Down Left Right Enter Escape Tab Space 0-9 y n`;
ALL keys validated before ANY is sent; **0** sent / **2** invalid session or
key, nothing sent / **3** gone / **6** send failed on a live session);
usage-limit stop ("You've hit your session limit · resets HH:MM") → if the
pane shows automatic continue armed (`autoContinueAtUsageLimit`), the CLI
resumes by itself — nothing further to do; otherwise wait for the reset
time, then re-send a resume prompt that orders a state re-check
(git status / tests) before continuing; finished-but-silent (forgot
status-update) → send a prompt to emit the missing signal; auth/trust screen →
keys per the screen. Then relaunch watch.
- **2 — timeout** (prints `[watch] TIMEOUT (<budget>s, source=<source>)`): a
checkpoint, not a verdict — re-check each session with
`scripts/tmux-worker-stalled.sh lo-<n>` and `scripts/send-prompt.sh state
lo-<n>` — **9** (`unsubmitted`) is now one of the answers: the worker is
parked on an unsubmitted prompt, recover with `scripts/send-prompt.sh keys
lo-<n> Enter`, never a re-send — read panes, then relaunch watch with the
same target.
## Phase 4 — Implement + review (max 3 rework)
Deliver §2 (implement) to each session with `scripts/send-prompt.sh send lo-<n>
"<prompt>"` — **0** delivered, **4** queued behind a busy turn, **7** unconfirmed
(may still have been delivered — do not resend; cross-check with `wait`/`state`),
**8** lost (confirmed: two quiet observations plus a failed automatic resend —
safe to re-dispatch), **3** the session is gone, **2** the session name or prompt
was rejected. Branch on the exit code; stdout is exactly one token and stderr is
advisory context that must never be parsed. On **4**, `scripts/send-prompt.sh wait
lo-<n> [timeout]` blocks until the worker picks it up (**0** picked-up, **5**
deadline expired, **9** the worker is parked on an unsubmitted prompt: press
`keys lo-<n> Enter`, do not re-send). Then
`watch-status ... impl_done <N>`. *(Orca
substrate: `task-create` the implement Task, then `scripts/orca-worker-start.sh
--task <impl_task> --terminal <handle> --worktree id:<repoId>::<path>` to reuse
that task's existing session — a bare `--terminal` is rejected with
`terminal_worktree_mismatch` (see O3) — and wait with `scripts/orca-wait.sh`.
Rework rounds are further Tasks on the same `--terminal`+`--worktree` pair.)*
Before the four-lens pass, run the floor: `scripts/test-floor.sh <wt> '<integ>'`.
At review time the worker has not committed (§2 forbids it; §4 commits
later), so the single-ref form measures the working tree — including
untracked new test files — instead of an empty `<integ>...HEAD` range; an
empty measurement now exits 2 (`unknown`, stderr `empty-range`), never
`pass`. **Exit 3** — skip the four-lens pass and the auditor
entirely; the itemized stderr reasons (`no-tests` / `case-count:<file>:<n>` /
`no-assertion:<file>:<case>`) become the findings of `reviews/<task>-rN.md` —
this consumes a rework round exactly like any other finding (run the rework
sequence below). **Exit 0 or 2** — continue to the four-lens pass unchanged,
and when the auditor is invoked, pass `floor=pass` or `floor=unknown`
alongside it.
Run the fixed four-lens pass on each worktree diff (`git -C <wt> diff
<integ>...HEAD`) — write the result to `reviews/<task>-rN.md` from
`templates/review-report.md`:
1. **Plan conformance** — diff vs. the plan's decision→page map and the
brief's `<scope_boundaries>` / `<out_of_scope>`; a decision silently made
differently at implement time is a defect even when the code works.
2. **Wiki re-route from the diff** — run AGENTS.md routing protocol step 7 on
the diff itself; report any page reached that the plan never named.
3. **Execution-environment reality** — any new flag/subcommand/API/dependency:
confirm it exists in the version present where the code actually runs
(`wiki/platforms/toolchains/flag-availability-at-the-execution-site.md`).
4. **Multi-object write ordering** — 2+ files/objects/rows written without a
transaction; any ordering a concurrent reader could observe mid-flight
(`wiki/backend/common/storage/multi-object-write-ordering.md`). You are the
only reviewer who sees every worktree at once, so cross-task ordering
hazards are your job alone.
Alongside the pass, if a session's tests look weak, **cross-call
`test-quality-auditor` yourself** (self-call + orchestrator cross-call).
On shortfall, decide rework: `STATUS_DIR=.orchestration/status
scripts/status-update.sh <task> rework` atomically increments `.attempt`;
read the NEW value N from `status/<task>.json`, write `reviews/<task>-rN.md`
(N is that counter value, so the filename and the counter can never
disagree), inject §3 (rework), repeat. The rework budget is
**`ready-set.sh`'s job**, not tracked here: `LO_MAX_REWORK` (default 3) fails
a non-terminal task for scheduling once its `.attempt` reaches it, and with
nothing else in flight that surfaces as **exit 3 DEADLOCK** naming the
exhausted task — Phase 3 step 1 already routes that to a human decision.
When a task is approved, merge it into the integration
branch first (Phase 3 step 5's merge-on-approval rule) before you
return to step 1 of the dispatch loop — whatever dependency it released shows up in the next
`ready-set.sh` round already merged, and the freed slot is refilled immediately.
When `ready-set.sh` returns **5**, go to Phase 5.
**Insight emission.** After a rework round's fix is confirmed by re-review,
emit one ★ Insight candidate per finding that was fixed and confirmed —
**only** for findings that sat in `## Findings` (they carried a failure
scenario: a real defect). `## Non-blocking` items never emit — they lack a
failure scenario, so they are style/preference, not the near-miss lesson this
rule captures. Both conditions must hold: the finding was a `## Findings`
item AND the following re-review round confirmed the fix — a finding that is
caught but not yet fixed is not a near-miss lesson yet. Use the frozen block
format from `hooks/insight-instruction.sh` verbatim (`trigger`/`directive`
required, `why`/`evidence` expected); the 0–3-per-session cap still applies,
so if a round confirms more fixes than the remaining budget, prioritize the
highest-signal finding. Map the fields like this:
```
★ Insight ─────────────────────────────────────
trigger: <diff signal in lens vocabulary — e.g. "new CLI flag, no version check">
directive: <reviewer's action on that signal — e.g. "confirm the flag exists in the deployed toolchain version">
why: <why the miss happened — e.g. "lens 3 exists for exactly this, the first pass skipped it">
evidence: reviews/<task>-rN.md + the fixing commit
─────────────────────────────────────────────
```
## Splitting a task mid-run
A worker may report that its task is much larger than the brief assumed. It
proposes; **you decide**, and you reply either way — a rejection that is never
sent is indistinguishable from silence, and a worker that hears nothing decides
for itself.
The proposal must carry, per piece, the `files` it would touch and the `outputs`
it would newly produce. Without them there is nothing to judge; ask for them
rather than guessing, and say the worker should hold.
**The decision is one overlap test.** Compare the proposed pieces' `files`
against every task that is currently dispatched and every task still pending in
`graph.json`:
- **No overlap** — add it as an independent node. `scripts/graph-add.sh
.orchestration/graph.json '<node-json>'` with `split_of` naming the parent and
`deps` carrying whatever the piece genuinely consumes. It enters the ready set
and the next free slot picks it up, so the split buys real parallelism.
- **Overlap** — add it with `deps: ["<parent>"]` and give it to the **same worker**
in the **same worktree** when the parent settles (Orca:
`worker-start --task <new> --terminal <handle> --worktree id:<repoId>::<path>`
— a bare `--terminal` is rejected with `terminal_worktree_mismatch` (see O3);
tmux: `send-prompt.sh send
lo-<n>`). Do **not** create a second worktree: the parent's code is not on the
integration branch until Phase 6, so a second checkout would be editing files
it cannot see. This split buys a smaller review and rework unit, not
parallelism — say so when you report it.
`graph-add.sh` returns **0** added, **3** REJECTED with the reason and the file
untouched, **4** the graph or the node could not be read. On **3**, reply to the
worker with the reason; do not retry the same node. A rejection for `depth 1`
means the proposal came from a piece that was itself a split — that is a signal
Phase 2's decomposition was wrong, so bring it to the user rather than working
around it.
On **4**, the graph file is unreadable (I/O error or corruption) — a failure
class different from validation. This blocks all dispatch. Reply to the worker:
"Split on hold — orchestrator cannot read its graph state. Escalating to user
immediately." Do not add the node. Report immediately to the user: "Graph I/O
error at `.orchestration/graph.json` — resolve and resubmit the proposal. Run is
blocked until `.orchestration/graph.json` is accessible."
You decide this without a user gate, but **report it immediately** — the task
list the user approved at Gate 1 just **grew** (split), and they need the
overlap verdict and the schedule change to intervene if they disagree.
**Dropping a task mid-run.** When a sibling's outcome makes an undispatched
node obsolete, drop it with `scripts/graph-drop.sh .orchestration/graph.json
.orchestration/status <task-id>`. It refuses a task that already has a status
file (any phase — dispatched, not yours to drop; that's the rework/failed
flow's job) and refuses a drop that would leave a dangling reference (another
task's `deps`, `split_of`, or `consumes` still names it) — drop dependents
before their parents. `graph-drop.sh` returns **0** dropped, **3** REJECTED
with the reason and the file untouched, **4** the graph or status dir could
not be read. You decide this without a user gate too, but **report it
immediately** — the Gate-1-approved task list just **shrank** (drop), and the
user needs the reason to intervene if they disagree.
## Blackboard — facts on disk, decisions on the hub
`.orchestration/notes/decisions.md` is an APPEND-ONLY file for cross-task facts:
an interface change to a declared output, or a load-bearing decision another
task could consume. Entry format, one line each: `- [<task-id>] <what changed
/ decided>`.
Workers read it at exactly two checkpoints — start of the implement phase, and
again before self-review — and append when they change a declared interface
or make a load-bearing decision; they never edit or delete an existing line,
even their own. The coordinator appends one line at each merge-on-approval
(Phase 3 step 5): `- [<task>] merged: <outputs> now on <integ>`.
Treat every line as a hint that something changed, not as the decision
itself: task assignment, rework routing, and merge approval stay on the
coordinator hub (status files, `ask-coordinator.sh`, Gate 1/Gate 2) — the
blackboard has no question path and settles nothing on its own
(`wiki/infrastructure/agent-orchestration/control-signals-vs-primary-artifacts.md`).
No helper script — a single shell primitive only: append with `printf '%s\n'
'- [<task-id>] <fact>' >> {ORCH_DIR}/notes/decisions.md`, `O_APPEND`-atomic
for a one-line write, and read the file with Read/`cat`. Never use Write/Edit
to append — both are read-modify-write, so a concurrent worker's stale read
silently drops another worker's just-appended line
(`wiki/backend/common/storage/multi-object-write-ordering.md`).
## Phase 5 — Integration test loop (max 3)
Merge-preview onto the integration branch and run the integration tests (use the
`verify` role's command if configured). On a test failure, route back to the
responsible session as rework.
Once tests are green, run the integration REVIEW via the `integration-reviewer`
agent (Agent tool, fresh context — not this coordinator session): pass it the
integration branch name, base ref, repo root, worktree paths, and the
`{ORCH_DIR}` paths of `graph.json`/briefs/plans/reviews; it runs `git diff
<base>...<integ>` itself. The coordinator consumes only its `VERDICT:
approve|rework` + `FINDINGS` + summary — MUST NOT read the full integration
diff into its own context at this phase (that full read is Gate 2's, the last
one — see **Coordinator token budget**). On `VERDICT: rework`, route each
finding back to its responsible task as rework, same as a failed integration
test. Repeat until the agent returns `VERDICT: approve`.
## 🚦 Gate 2 — pre-merge review (REQUIRED)
Show the full integration diff (`git diff`), then ask for the verdict with
**AskUserQuestion** (§ Asking the user) — merge / send back for rework / abort.
**Wait for the user's confirmation.**
## Phase 6 — Cleanup + merge (only after Gate 2)
1. `scripts/safe-cleanup.sh merge <root> <integ> <branch>...` — refuses dirty
worktrees, merges sequentially, stops + reports on conflict (no --force). A
branch already merged on approval (Phase 3 step 5, issue #90) re-merges here
as a no-op — git reports "Already up to date" — so this sweep stays correct
whether or not every branch was merged early.
2. `scripts/safe-cleanup.sh remove-worktrees <root> <branch>...` (after merge
verified; skips any dirty worktree).
3. `scripts/safe-cleanup.sh kill-sessions lo-<n>...` (exact names only), or — instead
of remembering every name — `scripts/safe-cleanup.sh sweep <root>`, the teardown
for ONE run: kill every tmux session named `lo-<n>-$LO_RUN_ID`, `git worktree
prune`, then report (never delete) any `.worktrees/` directory git does not know.
`sweep` REFUSES with exit 1, touching nothing, when `LO_RUN_ID` is unset or is not
`[A-Za-z0-9_-]+` — with no scope it would match every concurrent run's sessions.
**Orca substrate:** step 2 does not apply as written — an Orca-created worktree
is removed with `orca worktree rm --id <worktree.id> --json`, not
`remove-worktrees`, so Orca's own Run/Task state stays consistent with the
filesystem. `orca worktree rm` **also deletes the git branch**, so for every
branch `git merge-base --is-ancestor <branch> <integ>` MUST pass (exit 0)
**before** any `rm` — the same check an unforced `git branch -d` performs
internally — and there is no separate branch-deletion step afterward
(verified: 4 branches already gone after `rm`). Steps 1, 3, 4 are unchanged.
4. `scripts/safe-cleanup.sh list-orphans <root>` is read-only (kills, deletes and
prunes nothing; needs no `LO_RUN_ID`) — the census across ALL run ids, and the way
to read a dead run's id before sweeping it deliberately.
`--dry-run` may appear in any argument position on any destructive verb: it prints
exactly what the real run would touch and changes nothing, while refusals (dirty
worktree) still fire — a dry run never looks safer than the real one.
**Local merge into the feature branch only.** Remote push / PR is the user's job.
Steps 2–3 above (worktree removal, session sweep), plus archiving
`.orchestration/` artifacts, are exactly what `teardown` composes into one call
below — Phase 6's cleanup and the end-of-run contract cannot drift apart.
## End-of-run contract
Every orchestration run terminates in exactly one of three outcomes: **merged**
(Gate 2 passed, Phase 6 ran), **aborted** (the user or coordinator stopped the
run before Gate 2), or **escalated-and-abandoned** (a max-rework/max-retry
escalation was raised and the user chose not to continue). In all three, the
final step is the same: `LO_RUN_ID=<run-id> scripts/safe-cleanup.sh teardown
<root>` — it derives this run's worktrees from its own
`.orchestration/status/*.json`, removes them (dirty ones SKIPped, never
`--force`), sweeps this run's tmux sessions, and archives `.orchestration/`
artifacts to `archive-<date>-<runid>/`. A run that ends without teardown is
exactly the leak `list-orphans --stale` exists to find. On the Orca substrate
the same merge-verify-before-`worktree rm` rule applies to worktree removal
(see Phase 6's Orca substrate note); `safe-cleanup.sh teardown` still
archives `.orchestration/` and sweeps tmux-side leftovers regardless of
substrate.
After teardown, audit the run's token efficiency: `sh {SKILL}/scripts/token-report.sh
--cwd <root> .` for the coordinator's own session, plus one more `--cwd
<worktree>` per run worktree (each worker's transcripts live under its own
cwd). Put the table and any `warn:` lines in the final run report — a
peak-context warning is the signal to split tasks or delegate more next run.
Exit 0 is the report, exit 2 a usage error, exit 4 a malformed `LO_CTX_WARN`.
Basis: `wiki/infrastructure/agent-orchestration/session-context-token-budget.md`
(directive 1).
## Re-entry (resume)
On re-invocation with no context, measure real state first: `git worktree list`,
each `.orchestration/status/*.json` phase, and which `briefs/plans/reviews/`
artifacts exist. Resume from the earliest incomplete step (idempotently skip done
steps). There is no intermediate state such as a Wave index to restore. Reading
`.orchestration/graph.json` plus `status/*.json` and running `ready-set.sh` IS
the restored state — the same inputs always yield the same answer. The
rework round number is read from `status/<task>.json`'s `.attempt` field —
never reconstructed by globbing `reviews/<task>-r*.md`. Check `tmux ls`, and run `scripts/tmux-worker-stalled.sh <session>` on each
live one — a session that exists is not a worker that moves. Relaunch dead sessions and
re-deliver the right prompt with `scripts/send-prompt.sh send`. For leftovers of a
run that already died, `scripts/safe-cleanup.sh list-orphans --stale <root>`
enumerates them read-only (each with a `stale` verdict and its run id); tear a
dead one down with `LO_RUN_ID=<id> scripts/safe-cleanup.sh teardown <root>`.
On the Orca substrate, rebind the Run first (`orca orchestration run-use --id
<run_id> --json`), then measure with `orca orchestration task-list --json` +
`scripts/orca-worktree-alive.sh <wt>` **and** `scripts/orca-worker-stalled.sh <wt>`
— a Task reading `dispatched` proves only that it was handed out, never that the
worker is moving. Restart a proven-dead worker with a NEW Task (the old one is
spent, see O2) via `scripts/orca-worker-start.sh --task <new_task_id> --worktree
id:<...> --agent claude`; that call now probes first and rebinds to a live agent
terminal on that worktree instead of creating a second one, so re-running it is
safe (it exits 6 rather than guess when it cannot tell).
`setup-worktrees.sh` is idempotent (existing branches/worktrees are detected and
kept), so re-running it is safe. Note the difference from **partial resume** (Phase
0): that handles work done *outside* this orchestration — children with no
`.orchestration` record — whereas re-entry resumes this orchestration's own state.
## Guardrails
- You never implement — sessions do; you analyze, plan, review, manage.
- Gate 1 (task-split) and Gate 2 (pre-merge) are mandatory and are asked with
AskUserQuestion, never as a prose question; everything else autonomous.
- No remote push, no PR, no force-push. Destructive cleanup only after Gate 2, via
safe-cleanup (never --force).
- Sessions must not weaken tests (loop-implement guard); the auditor enforces it.
- No session or review agent may `git stash` — refs/stash is repository-global, so a
parallel worker's stash pop can silently swap another worker's uncommitted work
(issue #166); worker worktrees also carry a mechanical `Bash(git stash:*)` deny.
- Always verify real state after worktree/session ops (`git worktree list`, `tmux ls`,
status files) — never trust echo logs (set -e is fail-open in eval subshells).
- Bundled agents only: `test-quality-auditor`, `integration-reviewer`. Don't
depend on built-in agent names (general-purpose/Explore/Plan are
version-dependent).
- A completed/excluded issue (partial resume) is injected as a **base output**, never
silently dropped — otherwise its dependents lose their premise and re-create it.