From dc6e0f6e478d61f41eec74e36aa259911a40d612 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 01:56:59 -0400 Subject: [PATCH 1/4] docs(orchestration): give the kernel loop an exit condition and the missing commands D1: name the two liveness layers (worker-list projection.liveness is the fleet verdict, worker-show observation.status is PTY-only) and give the supervised loop a bounded stall procedure instead of an unbounded wait. D2: put worker-list, attention, requiresAction and nextAction in the loop and in completion accounting. D4: document request-show / --retry-request / terminal send --wait-submit. D5: check names its caller with --terminal, never --from. D7: give a dispatched worker a concrete follow-up read cadence. D10: document the real folder-workspace route (project setup-existing-folder). worker-start --spec is now the canonical loop's default. Guidance pins are contracts via squash() instead of reflow-fragile prose. --- .../orchestration-skill-guidance.test.mjs | 154 ++++++++++++++---- skill-guides/orchestration.md | 90 +++++----- .../references/messaging-and-gates.md | 10 +- .../references/placement-and-remote.md | 10 ++ .../references/recovery-and-cleanup.md | 46 +++++- .../references/worker-contract.md | 15 ++ src/cli/bundled-skill-guides.ts | 4 +- 7 files changed, 240 insertions(+), 89 deletions(-) diff --git a/config/scripts/orchestration-skill-guidance.test.mjs b/config/scripts/orchestration-skill-guidance.test.mjs index 6a1b6648e17..aa8972d235d 100644 --- a/config/scripts/orchestration-skill-guidance.test.mjs +++ b/config/scripts/orchestration-skill-guidance.test.mjs @@ -69,7 +69,7 @@ describe('orchestration kernel', () => { expect(kernel).toContain('Compatibility operator') expect(kernel).toContain('Ordinary terminal agent') expect(kernel).toContain('Model or effort selection does not make a handoff supervised') - expect(kernel).toContain('Never substitute a\nnon-Orca subagent tool') + expect(squash(kernel)).toContain('Never substitute a non-Orca subagent tool') }) it('makes Dispatch identity, remote uncertainty, folders, and mixed versions a safety floor', () => { @@ -78,10 +78,10 @@ describe('orchestration kernel', () => { expect(kernel).toContain('A Dispatch is one authoritative Task attempt') expect(kernel).toContain('Lifecycle authority comes from the active Dispatch') expect(kernel).toContain('execution host owns') - expect(kernel).toContain('`live` / `unverifiable` / `exited`') + expect(squash(kernel)).toContain('`live` / `unverifiable` / `exited`') expect(kernel).toContain('contact loss is not process death') expect(kernel).toContain('Folder workspaces are valid') - expect(kernel).toContain('Treat unknown optional fields\n as absent') + expect(squash(kernel)).toContain('Treat unknown optional fields as absent') expect(kernel).toContain('new stream operation requires advertised capability') expect(kernel).toContain('Never fall back to local execution') }) @@ -104,37 +104,75 @@ describe('orchestration kernel', () => { it('teaches worker-start as the only normal-path launch and starts the wave before waiting', () => { const kernel = readKernel() - const firstStart = kernel.indexOf('worker-start --task ') - const secondStart = kernel.indexOf('worker-start --task ') + const firstStart = kernel.indexOf('worker-start --spec ""') + const secondStart = kernel.indexOf('worker-start --spec ""') const firstWait = kernel.indexOf('check --wait') expect(firstStart).toBeGreaterThan(kernel.indexOf('run-create')) expect(secondStart).toBeGreaterThan(firstStart) expect(firstWait).toBeGreaterThan(secondStart) - expect(kernel).toContain('start the full independent wave before waiting') + expect(squash(kernel)).toContain('start the full independent wave before waiting') expect(kernel).toContain('`worker-start` is the normal path') expect(kernel).toContain('operator-created process unsupervised') expect(kernel).not.toMatch(/^ORCA terminal create/mu) }) + it('makes worker-start --spec the default and keeps task-create for planned fan-out', () => { + const kernel = squash(readKernel()) + + expect(kernel).toContain('`worker-start --spec` creates the Task and its attempt in one call') + expect(kernel).toContain('Use `task-create` plus `worker-start --task `') + }) + + it('gives the supervised loop an exit condition for a live terminal with a dead agent', () => { + const kernel = squash(readKernel()) + + expect(kernel).toContain("`worker-list`'s `projection.liveness` is the fleet verdict") + expect(kernel).toContain("`worker-show`'s `observation.status` is PTY liveness only") + expect(kernel).toContain('After three consecutive empty waits') + expect(kernel).toContain('`ORCA orchestration worker-list --json`') + expect(kernel).toContain('`requiresAction`, and literal `nextAction` argv') + expect(kernel).toContain('`attention`') + expect(kernel).toContain('reports `agentWait` null') + expect(kernel).toContain('choose `worker-stop` or `worker-abandon`') + }) + + it('names --terminal, never --from, as the check caller flag', () => { + const kernel = squash(readKernel()) + + expect(kernel).toContain('`check` names its caller with `--terminal `, never `--from`') + expect(kernel).not.toContain('check --from') + }) + + it('makes a dispatched worker read coordinator follow-ups on a cadence', () => { + const kernel = squash(readKernel()) + + expect(kernel).toContain('Read coordinator follow-ups at each natural checkpoint') + expect(kernel).toContain('once more immediately before `worker_done`') + expect(kernel).toContain('`ORCA orchestration check --terminal --json`') + }) + it('requires full Delivery processing and settled-terminal accounting before ack', () => { const kernel = readKernel() - expect(kernel).toContain('oldest FIFO Delivery and replays that\nbatch until acknowledged') - expect(kernel).toContain('Process every message') - expect(kernel).toContain("decide each\nsettled terminal's next owner before acknowledging") + expect(squash(kernel)).toContain( + 'oldest FIFO Delivery and replays that batch until acknowledged' + ) + expect(squash(kernel)).toContain('Process every message') + expect(squash(kernel)).toContain("decide each settled terminal's next owner before the ack") expect(squash(kernel)).toContain('reused, explicitly retained, or released') expect(kernel).toContain('worker-release --dispatch ') expect(kernel).toContain('check --ack --wait') - expect(kernel).toContain('Do not follow\nit with `task-update --status completed`') + expect(squash(kernel)).toContain('`worker-list --terminal-state reclaimable --json`') + expect(squash(kernel)).toContain('do not follow it with `task-update --status completed`') }) it('treats long waits and release uncertainty as safe checkpoints', () => { const kernel = readKernel() - expect(kernel).toContain('A timeout or empty result is\na checkpoint, not a failure') - expect(kernel).toContain('Do not stop, retry, release, or launch a duplicate\neditor') - expect(kernel).toContain('Never release because of\nidle state, timeout, heartbeat') + expect(squash(kernel)).toContain('A timeout or empty result is a checkpoint, not a failure') + expect(squash(kernel)).toContain('Do not stop, retry, release, or launch a duplicate editor') + expect(squash(kernel)).toContain('Never release because of idle state, timeout, heartbeat') expect(kernel).toContain('never substitute `terminal close`') }) @@ -152,7 +190,7 @@ describe('orchestration kernel', () => { } expect(kernel).toContain('successful `orchestration send` proves durable enqueue') expect(kernel).toContain('best-effort attention only') - expect(kernel).toContain('does not prove the recipient read the message') + expect(squash(kernel)).toContain('does not prove the recipient read or accepted it') }) }) @@ -164,13 +202,18 @@ describe('owned orchestration references', () => { .filter((name) => name.endsWith('.md')) .sort() - expect([...new Set(routed)].sort()).toEqual(shipped) - expect(routed).toHaveLength(shipped.length) - expect(kernel).toContain('ORCA skills get orchestration --full') - expect(kernel).toContain( - 'returns this exact kernel and every reference from\nthe same CLI build' + const tableRoutes = [...kernel.matchAll(/^\|.*`references\/([^`]+\.md)`.*\|$/gmu)].map( + (match) => match[1] ) - expect(kernel).toContain('If an older CLI rejects\n`--full`') + + expect([...new Set(routed)].sort()).toEqual(shipped) + // Why the table and not every mention: prose may cite a reference the gate table already routes. + expect(tableRoutes.sort()).toEqual(shipped) + expect(kernel).toContain('ORCA skills get orchestration --full') + expect(squash(kernel)).toContain( + 'returns this exact kernel and every reference from the same CLI build' + ) + expect(squash(kernel)).toContain('If an older CLI rejects `--full`') }) it('owns expanded waves, launch preferences, reuse, and review boundaries', () => { @@ -182,7 +225,9 @@ describe('owned orchestration references', () => { expect(reference).toContain('`launch.requested` with `launch.effective`') expect(reference).toContain('worker-start --task --terminal') expect(reference).toContain('A review-only `worker_done` authorizes synthesis') - expect(reference).toContain('post-review fixes and\nPR preparation remain with that owner') + expect(squash(reference)).toContain( + 'post-review fixes and PR preparation remain with that owner' + ) }) it('owns worker heartbeat, ask resume, escalation, failure, and idle', () => { @@ -197,6 +242,13 @@ describe('owned orchestration references', () => { expect(reference).toContain('Send exactly one terminal report') expect(reference).toContain('Use `--outcome failed`') expect(reference).toContain('After `worker_done`, end the dispatched turn and idle') + expect(squash(reference)).toContain( + 'ORCA orchestration check --terminal --json' + ) + expect(squash(reference)).toContain('once more immediately before `worker_done`') + expect(squash(reference)).toContain( + '`check` names its caller with `--terminal`, never `--from`' + ) }) it('keeps heartbeat and worker_done recipes bound to the injected capability', () => { @@ -212,7 +264,7 @@ describe('owned orchestration references', () => { } expect(workerDone).not.toContain('--files-modified') expect(workerDone).not.toContain('--report-path') - expect(reference).toContain('only when applicable, using actual\npaths') + expect(squash(reference)).toContain('only when applicable, using actual paths') expect(reference).toContain('Do not send documentation placeholders as metadata') }) @@ -224,18 +276,24 @@ describe('owned orchestration references', () => { expect(reference).toContain('--worktree new-top-level') expect(reference).toContain('Folder workspaces are first-class') expect(reference).toContain('Remote `current` and `new-child` are invalid') - expect(reference).toContain("`--on` selects\nonly the worker's execution server") - expect(reference).toContain('route every follow-up, read,\nstop, and cleanup by Dispatch ID') + expect(squash(reference)).toContain("`--on` selects only the worker's execution server") + expect(squash(reference)).toContain( + 'route every follow-up, read, stop, and cleanup by Dispatch ID' + ) expect(reference).toContain('`live`, `unverifiable`, or `exited`') - expect(reference).toContain('unknown stream\nopcodes can be silently dropped') + expect(squash(reference)).toContain('unknown stream opcodes can be silently dropped') expect(reference).toContain('printed `orca-ide`') + expect(squash(reference)).toContain( + 'ORCA project setup-existing-folder --project --host --path --kind folder --json' + ) + expect(squash(reference)).toContain('and rejects a plain directory') }) it('owns FIFO mail, Dispatch addresses, groups, questions, and gates', () => { const reference = readReference('messaging-and-gates.md') expect(reference).toContain('oldest FIFO Delivery') - expect(reference).toContain('Process\nevery row') + expect(squash(reference)).toContain('Process every row') expect(reference).toContain('send --to dispatch:') for (const group of ['@all', '@grok', '@cursor', '@worktree:']) { expect(reference).toContain(group) @@ -244,7 +302,10 @@ describe('owned orchestration references', () => { expect(reference).toContain('gate-create --task ') expect(reference).toContain("Do not create a gate merely to answer a worker's `ask`") expect(reference).toContain('successful `send` proves durable enqueue') - expect(reference).toContain('Wake and nudge are best-effort\nattention only') + expect(squash(reference)).toContain('Wake and nudge are best-effort attention only') + expect(squash(reference)).toContain( + '`check` names its caller with `--terminal ` and is the only verb that rejects `--from`' + ) }) it('owns positive-evidence retry, unknown outcomes, retain/release, and no terminal close', () => { @@ -254,12 +315,33 @@ describe('owned orchestration references', () => { expect(squash(reference)).toContain('| `outcome_unknown` | Inspect') expect(squash(reference)).toContain('| Remote contact lost | Preserve `unverifiable`') expect(reference).toContain('--retry-of ') - expect(reference).toContain('Placement is never\nsilently inherited') + expect(squash(reference)).toContain('Placement is never silently inherited') expect(reference).toContain('worker-abandon --dispatch') expect(reference).toContain('worker-retain --dispatch') expect(reference).toContain('worker-release --dispatch') - expect(reference).toContain('`release_pending`\nor `release_unknown`') - expect(reference).toContain('Never substitute\n`terminal close`') + expect(squash(reference)).toContain('`release_pending` or `release_unknown`') + expect(squash(reference)).toContain('Never substitute `terminal close`') + }) + + it('owns the lost-response question and the request-show verdicts', () => { + const reference = squash(readReference('recovery-and-cleanup.md')) + + expect(reference).toContain('request-show --request --json') + expect(reference).toContain('--retry-request ') + expect(reference).toContain('`completed` means the mutation already took effect') + expect(reference).toContain('`pending` means the original mutation is still running') + expect(reference).toContain('that is not proof nothing happened') + expect(reference).toContain('terminal send --wait-submit ') + }) + + it('names worker-list as the enumerating command and the agent-liveness authority', () => { + const reference = squash(readReference('recovery-and-cleanup.md')) + + expect(reference).toContain('ORCA orchestration worker-list --json') + expect(reference).toContain("`worker-show`'s `observation.status` is PTY liveness only") + expect(reference).toContain('`attention` categories, `requiresAction`') + expect(reference).toContain('`nextAction` argv') + expect(reference).toContain('the fleet verdict decides') }) it('owns the custom topology exception without claiming process ownership', () => { @@ -269,9 +351,9 @@ describe('owned orchestration references', () => { expect(reference).toContain('terminal create --worktree active') expect(reference).toContain('dispatch --task --to --inject') expect(reference).toContain('operator-created process unsupervised') - expect(reference).toContain('creates no supervised worker\nresource row') + expect(squash(reference)).toContain('creates no supervised worker resource row') expect(reference).toContain('Use `worker-start --terminal `') - expect(reference).toContain('never\nuse it for an ownership handoff') + expect(squash(reference)).toContain('never use it for an ownership handoff') }) it('owns legacy labels, read-only degradation, exact recovery, and takeover', () => { @@ -280,11 +362,11 @@ describe('owned orchestration references', () => { expect(reference).toContain('[LEGACY COMPATIBILITY]') expect(reference).toContain('[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]') expect(reference).toContain('[LEGACY READ-ONLY]') - expect(reference).toContain( - 'degrade to\nread-only inspection and never fall back to local execution' + expect(squash(reference)).toContain( + 'degrade to read-only inspection and never fall back to local execution' ) - expect(reference).toContain( - 'must not spawn, write, signal, stop, switch, focus, split, or\ninject' + expect(squash(reference)).toContain( + 'must not spawn, write, signal, stop, switch, focus, split, or inject' ) expect(reference).toContain('launcher status `75`') expect(reference).toContain('run_legacy_local') diff --git a/skill-guides/orchestration.md b/skill-guides/orchestration.md index be60ae106a8..079b7507d21 100644 --- a/skill-guides/orchestration.md +++ b/skill-guides/orchestration.md @@ -21,11 +21,9 @@ which attempt is authoritative, and when supervised work has settled. ## Outcome **Result:** every in-scope Task has one explicit outcome and every settled worker -terminal has a next owner or cleanup decision. - -**Done:** all expected Dispatches have settled, every delivered message was -processed before acknowledgment, and each settled worker was reused, explicitly -retained, or released. +terminal has a next owner or cleanup decision. **Done:** all expected Dispatches +have settled, every delivered message was processed before acknowledgment, and +each settled worker was reused, explicitly retained, or released. **Safe failure:** preserve work and authority and report the state as unknown or `unverifiable`. A timeout, quiet terminal, missing client, or lost remote @@ -56,12 +54,13 @@ claiming a worker was orchestrated, verify its Task and Dispatch exist. - After remote start, address the worker by Dispatch ID. The execution host owns process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts `live` / `unverifiable` / `exited`; contact loss is not process death. +- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict + for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live + terminal can still hold a dead or stuck agent. - Folder workspaces are valid. Do not require Git or assume every workspace is a worktree. -- When a command requires an exact worktree selector, use the full - `::` value returned by Orca; a bare repo id is not a worktree id. -- For a newly created workspace, pass that returned value as `id:`; - do not shorten it to the repository id. +- Worktree selectors need the full `::` value Orca returned, + passed as `id:`; a bare repo id is not a worktree id. - Clients and remote servers update independently. Treat unknown optional fields as absent. A new stream operation requires advertised capability because old decoders may silently drop unknown opcodes. Never fall back to local execution @@ -70,11 +69,9 @@ claiming a worker was orchestrated, verify its Task and Dispatch exist. examples below, replace `ORCA` with it; do not create a shell variable or run `ORCA` literally. If it fails, report that exact error instead of switching. - Legacy takeover binds the authenticated invoking terminal; `--from` cannot - nominate another coordinator. It preserves live work and fences the former - coordinator, so never take over while that coordinator is still active. -- A successful `orchestration send` proves durable enqueue. Its wake or nudge is - best-effort attention only; it does not prove the recipient read the message, - started a turn, or accepted steering. + nominate another coordinator. Never take over while that coordinator is active. +- A successful `orchestration send` proves durable enqueue; its wake or nudge is + best-effort attention only and does not prove the recipient read or accepted it. ## Worker obligations @@ -85,10 +82,13 @@ The injected preamble is authoritative. A dispatched worker must: answer. Resume the same message ID after an ask timeout. 2. Send heartbeats only at the cadence in the preamble. A heartbeat proves liveness, not completion. -3. Send `worker_done` exactly once, from the dispatched terminal, with a +3. Read coordinator follow-ups at each natural checkpoint — before starting a + new file, after a test run — and once more immediately before `worker_done`: + `ORCA orchestration check --terminal --json`. +4. Send `worker_done` exactly once, from the dispatched terminal, with a three-sentence executive summary, both lifecycle IDs, and explicit `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose. -4. Append `--files-modified` and `--report-path` only with real values when +5. Append `--files-modified` and `--report-path` only with real values when applicable. After `worker_done`, end the dispatched turn and idle; do not poll or start new work. @@ -105,27 +105,27 @@ coordinator-supervised follow-up arrives with a fresh preamble and Task block. ## Canonical supervised loop -Confirm the runtime, create or bind one Run, create all independent Tasks, and -start the full independent wave before waiting: +Confirm the runtime, bind one Run, and start the full independent wave before +waiting. `worker-start --spec` creates the Task and its attempt in one call: ```text ORCA status --json ORCA orchestration run-create --objective "" --json -ORCA orchestration task-create --spec "" --json -ORCA orchestration task-create --spec "" --json -ORCA orchestration worker-start --task --worktree current --agent codex --json -ORCA orchestration worker-start --task --worktree current --agent claude --json +ORCA orchestration worker-start --spec "" --worktree current --agent codex --json +ORCA orchestration worker-start --spec "" --worktree current --agent claude --json ORCA orchestration check --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json ``` -Use Task dependencies only for real ordering. Prefer parallel waves over chains -deeper than three or four steps. Nested workers obey the configured depth limit; -creating another Run does not reset the caller's depth. +Use `task-create` plus `worker-start --task ` for planned fan-out with +dependencies or a retry of a known Task. Use dependencies only for real ordering +and prefer parallel waves over chains deeper than three or four steps; nested +workers obey the depth limit, and a new Run does not reset the caller's depth. -A consuming `check` returns the bound Run's oldest FIFO Delivery and replays that -batch until acknowledged. Process every message. Reply to questions, validate -that each `worker_done` belongs to the expected active Dispatch, and decide each -settled terminal's next owner before acknowledging: +A consuming `check` names its caller with `--terminal `, never `--from`; +omit it inside the coordinator's own Orca terminal. It returns the bound Run's +oldest FIFO Delivery and replays that batch until acknowledged. Process every +message: reply to questions, validate each `worker_done` against the expected +active Dispatch, and decide each settled terminal's next owner before the ack: ```text ORCA orchestration reply --id --body "" --json @@ -137,10 +137,16 @@ Keep waiting until every expected Dispatch settles. A timeout or empty result is a checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate editor from timeout, idle state, heartbeat, relay loss, or missing client alone. -`worker-start` is the normal path. It composes placement, terminal readiness, -prompt injection, and supervised resource ownership. Low-level -`dispatch --inject` is reserved for an expressiveness gap and leaves an -operator-created process unsupervised. +After three consecutive empty waits, stop waiting blindly and enumerate with +`ORCA orchestration worker-list --json`. Act on each row's `attention`, +`requiresAction`, and literal `nextAction` argv. A worker that is not `live`, +whose `worker-show` reports `agentWait` null, and whose `worker-read` shows no +new progress is stalled, not working: load `references/recovery-and-cleanup.md` +and choose `worker-stop` or `worker-abandon` explicitly. + +`worker-start` is the normal path, composing placement, terminal readiness, +prompt injection, and supervised resource ownership. `dispatch --inject` leaves +an operator-created process unsupervised and is only for an expressiveness gap. ## Task-spec contract @@ -162,22 +168,22 @@ After an accepted success or failure report, immediately do exactly one: Release is post-settlement cleanup, not cancellation. Never release because of idle state, timeout, heartbeat, status, question, escalation, or a rejected or -stale completion. If release is uncertain, follow its exact recovery receipt; -never substitute `terminal close`. Released output remains readable through +stale completion. If release is uncertain, follow its exact recovery receipt and +never substitute `terminal close`. Released output stays readable via `worker-read`. -A valid `worker_done` settles the Task and Dispatch automatically. Do not follow -it with `task-update --status completed`. Do not end the coordinator turn until -all expected Dispatches and settled terminals are accounted for. +A valid `worker_done` settles the Task and Dispatch automatically; do not follow +it with `task-update --status completed`. Enumerate the terminals still owing a +decision with `worker-list --terminal-state reclaimable --json`, and do not end +the coordinator turn until it returns none. ## Conditional references This compact guide is sufficient for the normal local loop. At an action gate below, run `ORCA skills get orchestration --full` once and read only the named -bundled reference. `--full` returns this exact kernel and every reference from -the same CLI build in one deterministic document. If an older CLI rejects -`--full`, keep this kernel's safety floor and use that command's `--help`; do not -guess newer flags. +bundled reference: it returns this exact kernel and every reference from the same +CLI build. If an older CLI rejects `--full`, keep this kernel's safety floor, use +that command's `--help`, and never guess newer flags. | Action gate | Bundled reference | | ----------------------------------------------------------------------------------- | ----------------------------------------- | diff --git a/skill-guides/orchestration/references/messaging-and-gates.md b/skill-guides/orchestration/references/messaging-and-gates.md index b845d460871..aa7ff488a08 100644 --- a/skill-guides/orchestration/references/messaging-and-gates.md +++ b/skill-guides/orchestration/references/messaging-and-gates.md @@ -10,11 +10,16 @@ accepted steering. ## Coordinator delivery loop ```text -ORCA orchestration check --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json +ORCA orchestration check --terminal --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json ORCA orchestration reply --id --body "" --json ORCA orchestration check --ack --wait --types "worker_done,escalation,question" --timeout-ms 900000 --json ``` +`check` names its caller with `--terminal ` and is the only verb that +rejects `--from`. Omit `--terminal` inside an Orca terminal, where Orca resolves +the caller; pass it explicitly from anywhere else, including a dispatched +worker reading coordinator follow-ups. + A consuming coordinator `check` returns the bound Run's oldest FIFO Delivery, up to 50 messages, and replays that exact batch until acknowledged. Process every row and required terminal ownership decision before `--ack`. Type filters @@ -35,7 +40,8 @@ ORCA orchestration send --to dispatch: --subject "Follow-up" --body Do not substitute a remote terminal handle. Omit `--from` for ordinary coordinator calls; a dispatched worker instead copies the exact `--from` and -capability arguments in its preamble. +capability arguments in its preamble. `check` is the exception: it identifies +its caller with `--terminal`, never `--from`. Group addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`, `@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:`. Use them only for diff --git a/skill-guides/orchestration/references/placement-and-remote.md b/skill-guides/orchestration/references/placement-and-remote.md index 6fb19cf669c..d1390dc76b3 100644 --- a/skill-guides/orchestration/references/placement-and-remote.md +++ b/skill-guides/orchestration/references/placement-and-remote.md @@ -25,6 +25,16 @@ Current and exact existing workspaces create a fresh terminal unless `--terminal` is explicit. Folder workspaces are first-class; do not invoke Git or require worktree lineage when the selected workspace is a folder. +Register a folder workspace through project setup. `repo add --path ` +requires a valid Git repository and rejects a plain directory: + +```text +ORCA project setup-existing-folder --project --host --path --kind folder --json +``` + +Then place work on the returned workspace with an exact selector. `new-child` +and `new-top-level` are worktree creation and do not apply to a folder. + New worktrees use agent-first creation and run setup by default. Preserve the repository's startup policy: `start-immediately` can report setup as `running`, while `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base, diff --git a/skill-guides/orchestration/references/recovery-and-cleanup.md b/skill-guides/orchestration/references/recovery-and-cleanup.md index b3ebef4203c..ec2107ef3ff 100644 --- a/skill-guides/orchestration/references/recovery-and-cleanup.md +++ b/skill-guides/orchestration/references/recovery-and-cleanup.md @@ -3,21 +3,31 @@ Load this reference only after a failed/stopped/unknown attempt, explicit retry decision, stop/abandon request, retention request, or uncertain release. -| Proven state | Safe action | -| ---------------------- | ------------------------------------------------------------------ | -| `ready` or active | Keep waiting; optionally read bounded output | -| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly | -| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` | -| Accepted `worker_done` | Reuse, retain, or release | -| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone | +| Proven state | Safe action | +| ----------------------- | ------------------------------------------------------------------ | +| `ready` or active | Keep waiting; optionally read bounded output | +| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly | +| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` | +| Accepted `worker_done` | Reuse, retain, or release | +| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone | +| Live PTY, stalled agent | Enumerate with `worker-list`; follow its `nextAction` | ## Inspect before acting ```text +ORCA orchestration worker-list --json ORCA orchestration worker-show --dispatch --json ORCA orchestration worker-read --dispatch --limit 50 --json ``` +`worker-list` is the enumerating command and the authority on agent liveness: +each row carries `projection.liveness`, `attention` categories, `requiresAction`, +and a literal `nextAction` argv to run. `worker-show`'s `observation.status` is +PTY liveness only, so a `live` terminal whose agent died at a trust prompt still +reads `live` there. When the two disagree, the fleet verdict decides. A worker +that is not `live`, reports `agentWait` null, and shows no new `worker-read` +progress is stalled: stop waiting and choose `worker-stop` or `worker-abandon`. + `worker-read --source auto` uses a proven provider transcript when available and otherwise returns bounded terminal output with a typed `fallbackReason`. Continue with its top-level cursor, which is pinned to that source. If Orca @@ -27,6 +37,28 @@ read `contentComplete`, `clipping`, and `warnings` before assuming omitted older records are pageable. Never guess a provider session ID, transcript path, or remote terminal handle. +## Was the mutation applied? + +When a mutation's response was lost and named no Dispatch, do not replay blind. +Every orchestration mutation accepts `--retry-request `, which reuses one +operation identity so Orca can replay, join, or recover it instead of starting a +duplicate. Ask what happened first: + +```text +ORCA orchestration request-show --request --json +``` + +`completed` means the mutation already took effect; read its recorded receipt +instead of rerunning. `pending` means the original mutation is still running or +Orca restarted before recording its outcome; replay the original command with +`--retry-request `. `absent` means this runtime holds no receipt +under your caller identity — that is not proof nothing happened, so inspect the +affected Task, Dispatch, and terminal before deciding whether to retry. + +When a worker's terminal accepted input but the submit is unconfirmed, use +`terminal send --wait-submit `: it observes the accepted prompt for that +long and, on timeout, returns the input-accepted receipt without resending. + ## Retry, stop, and abandon Retry only a positively proven failed or stopped attempt. Placement is never diff --git a/skill-guides/orchestration/references/worker-contract.md b/skill-guides/orchestration/references/worker-contract.md index 4659a871c66..49444687d78 100644 --- a/skill-guides/orchestration/references/worker-contract.md +++ b/skill-guides/orchestration/references/worker-contract.md @@ -30,6 +30,21 @@ ORCA orchestration ask --from --dispatch-capability A timeout or disconnect leaves the original question pending. Resume its message ID; do not create a duplicate question. +## Reading coordinator follow-ups + +The coordinator steers a running worker with `send --to dispatch:`. That +enqueue is durable but does not interrupt you, so nothing arrives unless you +look: + +```text +ORCA orchestration check --terminal --json +``` + +Run it at each natural checkpoint — before starting a new file, after a test +run — and once more immediately before `worker_done`, so a redirect or a +cancellation lands before the Task settles. `check` names its caller with +`--terminal`, never `--from`. Stop checking after `worker_done`. + ## Escalation Escalate only before completion and only when the coordinator must intervene: diff --git a/src/cli/bundled-skill-guides.ts b/src/cli/bundled-skill-guides.ts index e7f1fa498f1..d3ae3e5cb36 100644 --- a/src/cli/bundled-skill-guides.ts +++ b/src/cli/bundled-skill-guides.ts @@ -30,10 +30,10 @@ const ORCA_LINEAR_MARKDOWN = "---\nname: orca-linear\ndescription: >-\n Use Orc const ORCA_PER_WORKSPACE_ENV_MARKDOWN = "---\nname: orca-per-workspace-env\ndescription: >-\n Set up, review, debug, or validate Orca per-workspace environment recipes —\n on-demand, disposable runtimes (cloud sandboxes, VMs, or local) created fresh\n for each workspace. Covers first-time setup (provider prerequisites, the\n reusable base snapshot, the coding-agent auth snapshot, credentials, and\n state), not just the per-workspace lifecycle scripts. Use to stand up\n per-workspace environments, fix an `environmentRecipes` entry in `orca.yaml`, scaffold\n provider lifecycle scripts, or resolve an `orca vm recipe doctor` failure.\n---\n\n# Per-Workspace Environments\n\nHelp a user stand up and maintain a repo-owned per-workspace environment recipe end to end. Each\nworkspace gets its own on-demand, disposable runtime (a cloud sandbox, a VM, or a local one),\ncreated fresh and torn down after.\n\nOrca is a **thin wrapper**: you guide, detect, and scaffold; you never own the user's cloud account,\nbilling, images, or credentials.\n\n- **You DO:** sequence the setup, detect what's detectable (provider CLI present/logged-in? recipe\n present? `doctor` passing?), scaffold provider-templated scripts the user fills in, drive the slow\n snapshot/auth phases with the user, and always show the next action.\n- **You DO NOT:** create accounts, choose plans/regions, invent org/project/scope ids, store or print\n secrets, or run anything that spends money without an explicit user OK.\n\nFirst-time setup has **four phases before the per-workspace recipe runs** — easy to miss, so walk\nthem in order:\n\n1. **Prerequisites** — cloud account, provider CLI, scope/project, plan limits, git token (§2).\n2. **Base snapshot** — reusable image: tools + repo + headless build, snapshotted once (§3).\n3. **Agent-auth snapshot** — boot the base, run interactive device-auth, re-snapshot (§4).\n4. **State** — thread snapshot id / scope / project / port between phases via a state file (§6).\n\nThen the **per-workspace contract** (create/suspend/resume/destroy) runs fast (§8).\n\n**The one branch that shapes everything — connection mode:** **Orca-server** (`create` runs `orca serve`\nin the env and emits a `pairingCode`; §7c/§7f) vs **SSH** (`create` runs no server and emits a\n`connection.type:\"ssh\"` block Orca dials into; §7g/§7h). Settle this first — it changes the `create`\noutput shape and half the templates.\n\nKeep Orca's checkout behavior unchanged by default: omit `checkoutMode`, emit schema version 1, and\nlet Orca create a linked worktree. Only use `checkoutMode: provisioned-root` when the user explicitly\nwants one ephemeral machine to clone the finished workspace itself. This niche mode currently requires\ndirect SSH, an ordinary non-bare/non-sparse primary checkout at `projectRoot`, and schema version 2.\n\n**Quick-start (happy path):** interview the user (connection mode Orca-server vs SSH, provider, agent CLI,\ngit auth — §1.2) + read the provider's CLI docs → scaffold `scripts/orca-vm/` from §7 → run the\nbase-snapshot script, then the auth script (you invoke these by hand; not via `orca.yaml`) → wire\n`environmentRecipes` in `orca.yaml` → `orca vm recipe doctor --json` (free) → then the `--provision`\nself-test loop (§9) until it passes.\n\n---\n\n## 1. Setup workflow\n\nDrive these with the user. **[CHECKPOINT]** steps need explicit confirmation — they spend money, take\na long time, or need the user at the keyboard. Never create an Orca workspace or commit unless asked.\n\n1. **Inspect the repo** for an existing `environmentRecipes` entry, `scripts/orca-vm/`, a state file, or setup\n notes. If a working recipe exists, jump to Doctor (§9) instead of rebuilding.\n2. **Interview the user up front** — gather these choices and confirm them back before scaffolding\n anything. Don't pick for them (§11); don't guess.\n - **Connection mode:** how Orca attaches to the environment — an **Orca server** (the VM runs\n `orca serve` and Orca pairs over its pairing URL; worked example §7f) or **SSH** (Orca connects to\n the host over SSH; §7g). This decides the recipe's connection shape, so settle it first.\n - **Checkout ownership:** do not ask by default. Only when the user requires the environment to\n create the exact final checkout, confirm `provisioned-root` and direct SSH; otherwise omit it.\n - **Provider:** Vercel Sandbox, Fly, Modal, an existing SSH host, … For non-obvious providers, also\n ask scope/project/region and plan limits (§2). Then **read that provider's CLI/SDK docs** (or\n ` --help`) before scaffolding — you need its exact create/exec/snapshot/remove verbs.\n If a provider advertises `ssh`, verify whether it exposes a real dialable SSH target\n (host/port/user/key or proxy command) or only a provider-mediated interactive shell; Orca SSH mode\n needs the former.\n - **Coding-agent CLI + account:** which agent runs in the VM (`codex`, `claude`, …) and that the user\n has an account for it — it gets logged in during the Phase-3 auth snapshot (§4).\n - **Git auth:** the token source for cloning a private repo (`GH_TOKEN`/`GITHUB_TOKEN` or `gh auth\ntoken`; §5).\n3. **Check prerequisites (§2)** — detect the provider CLI + auth and confirm the items above are in\n place before any paid step.\n4. **Scaffold scripts + state file** from §7 (worked Vercel example: §7f; SSH host: §7g; Docker SSH:\n §7h; Windows: §7i), filling in the provider's real commands. Make them executable.\n5. **[CHECKPOINT] Build the base snapshot (§3)** — paid, slow.\n6. **[CHECKPOINT] Authenticate the agent (§4)** — interactive; the user follows a URL/code. **You cannot\n drive this step** — you run commands non-interactively, so there's no TTY for `docker exec -it` /\n `ssh -t` to prompt against. The **user** runs the Phase-3 login in their own terminal (or via the\n Claude Code harness bang-prefix — `! `, with the required space after `!`); you scaffold and drive\n the non-interactive phases around it. After kicking it off, **ask the user to report back once the login\n finishes** — you can't observe it completing, and you need that confirmation before resuming the\n non-interactive steps (base/auth commit, doctor, provision).\n7. **Wire the recipe** so `orca.yaml` points create/suspend/resume/destroy at the scripts (§8). The\n workspace composer reads `environmentRecipes` from the project's primary checkout of `orca.yaml`, **not** from\n a feature branch or worktree. So a recipe added only on a branch won't appear as a \"Run on\" option\n until that `orca.yaml` change is committed and merged to the project's primary branch. Tell the user\n this up front: `doctor`/`--provision` validate the scripts from the working copy on any branch, but\n creating a workspace from the recipe in the picker needs it on primary.\n8. **Dry-run doctor** — `orca vm recipe doctor --repo-path --json` (free, static; §9).\n Fix every failure before going live.\n9. **[CHECKPOINT] Live self-test** — get the user's OK once, then run\n `orca vm recipe doctor --provision --json` as a loop: it runs create → validates →\n destroys, and on failure returns a full transcript. Read it, fix the scripts, and re-run yourself until\n it passes (§9). Spends cloud money; the one approval covers the loop.\n10. **[CHECKPOINT] Optional workspace test** — only if asked: create a workspace via the picker, then\n verify sleep/wake/delete.\n\n---\n\n## 2. Phase 1 — Prerequisites\n\nThe user's responsibility; verify what's verifiable, ask for the rest, invent nothing. State which\nitems you verified vs. which the user asserted.\n\n- **Connection mode** (Orca server vs SSH) confirmed with the user — see §1 step 2; it shapes the recipe.\n- **Cloud account + plan** that allows sandboxes/VMs. Ask.\n- **Provider CLI installed + authenticated** — detect (`command -v `), check auth (e.g.\n `vercel whoami`). If missing, point at the provider's docs; don't log them in.\n- **Scope / project / region** the sandboxes live under. Ask; flows into every script via state.\n- **Plan / timeout / RAM caps.** Record them — e.g. Vercel Hobby caps sandbox timeout at **45m**,\n which limits both the base build and per-workspace runtime (see §10).\n- **Git token for private repos** (`GH_TOKEN`/`GITHUB_TOKEN`, or the provider's git auth; can fall back\n to `gh auth token`). See §5.\n- **Coding-agent CLI choice** (`codex`, `claude`…) and that the user has an account — it gets\n authenticated into the VM in Phase 3.\n\n---\n\n## 3. Phase 2 — Base snapshot (the reusable image)\n\nBuild **once**, snapshot, and every workspace boots from it in seconds instead of rebuilding.\nProvisioning + building takes a while (often ~20–30 min), so it runs behind a checkpoint. The script\nshape is §7a; key points:\n\n- Build the **headless Electron main only** (not the renderer) so it fits in plan RAM.\n- Use the VM image's package manager (`apt`/`dnf`/`apk`, per the base distro — not the provider brand).\n- Clone with the git token via `GIT_ASKPASS` (§5).\n- **Trap errors and remove the half-built sandbox** so a crash doesn't leave a paid resource running.\n- **Never snapshot a machine on which the Orca runtime has already run.** The first `orca serve` creates\n the runtime's user-data dir, and everything in it gets baked into the image and shared by every VM\n booted from it: the pairing keypair and device-token registry (`orca-devices.json`,\n `orca-e2ee-keypair.json`), `agent-session-authority.key`, and the build box's logs, terminal history\n and orchestration db. Confirmed: two VMs from one such snapshot emitted **identical `deviceToken` and\n `pairedDeviceId`**. Snapshot **before** the runtime has ever run, or delete the resolved user-data\n directory first: `orca_user_data_path=\"${ORCA_USER_DATA_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/orca}\"; rm -rf -- \"$orca_user_data_path\"`.\n This matches Orca's Linux precedence for custom and default paths; deleting a named file list will\n drift as Orca adds state.\n- Snapshot the stopped sandbox, parse the snapshot id, and write it + scope/project/port/repo to state.\n\n---\n\n## 4. Phase 3 — Agent-auth snapshot (interactive)\n\nThe base snapshot has the agent CLI installed but **not logged in**, and per-workspace VMs are\nephemeral — so authenticate once and bake it into a second snapshot layer. Script shape is §7b:\n\n1. Boot a sandbox from the base `snapshotId` (from state).\n2. Run the agent's login **interactively** (`--interactive --tty`); the user completes the URL/code in\n their browser. On a **headless VM this must be the device-auth flow** (e.g. `codex login --device-auth`),\n **not** plain `codex login`: the default OAuth login starts a loopback callback server on a container\n port the host browser can't reach, so it hangs. Device-auth instead prints a URL + code the user opens\n on the **host**.\n3. Verify login; **refuse to snapshot an unauthenticated VM.** Prefer the status command's **exit code**\n (most agent CLIs exit non-zero when unauthenticated). If you grep instead, agent status often goes to\n **stderr** (e.g. `codex login status` prints \"Logged in using ChatGPT\" there), so **fold stderr first**\n (`... 2>&1 | grep …`) and match the agent's **exact success line** — never `grep -qi 'logged in'`, which\n also matches \"**not** logged in\" and would commit an unauthenticated image.\n4. Re-snapshot, parse the new id, and overwrite `snapshotId` in state to the authenticated image\n (recording `authSourceSnapshotId`). Remove the auth sandbox.\n\n**You can't drive step 2 yourself** (you run commands non-interactively — no TTY). The **user** runs it in\ntheir own terminal, or via the Claude Code harness bang-prefix (`! `, with the required space after\n`!`). You scaffold/boot the sandbox and run steps 3–4, but **you cannot observe the interactive login\nfinishing** — so **ask the user to tell you when it's done** before you verify and re-snapshot.\n\nThis layer inherits §3's rule: if you started `orca serve` on the base or auth sandbox to smoke-test it,\ndelete the runtime's user-data dir (`~/.config/orca` on Linux) before re-snapshotting, or every workspace\nbooted from this image shares one pairing identity and one `agent-session-authority.key`.\n\nIf the agent's credentials are short-lived, warn that the snapshot may need periodic re-auth (§10).\n\nFor disposable runtimes, do **not** treat a host agent config directory (for example `~/.codex`) as the\nauth snapshot by bind-mounting or copying it wholesale. Agent homes often contain sqlite state, hook\napproval state, caches, logs, and host-specific env/config. Instead, authenticate/configure the agent\ninside the disposable runtime and snapshot/commit that runtime layer.\n\n---\n\n## 5. Credentials\n\n- **Never** commit secrets or put them in `userData`, recipe JSON, comments, docs, or the state file.\n- **Git token:** read from env (`GH_TOKEN`/`GITHUB_TOKEN`), falling back to `gh auth token`. Pass to the\n VM only via the provider's ephemeral `--env`. Inside the VM, use a `GIT_ASKPASS` helper with\n `x-access-token` (not the token in the clone URL) and `GIT_TERMINAL_PROMPT=0` so a missing token fails\n fast instead of hanging. When you write the helper from inside `bash -lc` under `set -u`, escape the\n positional arg and the token (`\\$1`, `\\$GH_TOKEN`) so they land **literally** and resolve at git-runtime\n — an unescaped `$1` aborts with \"unbound variable\", and a literal `$GH_TOKEN` keeps the real token out of\n the written file. `rm -f` the helper after the clone/fetch.\n- **Provider auth:** rely on the provider CLI's logged-in session, not checked-in keys.\n- **Agent auth:** lives in the authenticated snapshot (Phase 3) — never a file you write or commit.\n- State holds only **non-secret** wiring (snapshot ids, scope, project, port, repo url/ref).\n\n---\n\n## 6. State file\n\nA repo-local JSON file (e.g. `scripts/orca-vm/-state.json`) threads non-secret values between\nphases. Each script resolves values as **env var → state → built-in fallback**, and merges its outputs\nback. Phase 2 writes the base `snapshotId`; Phase 3 overwrites it with the authenticated snapshot;\nper-workspace `create` boots from `snapshotId`.\n\n```json\n{\n \"baseName\": \"orca-base\",\n \"snapshotId\": \"snap_authenticated_image_id\",\n \"authSourceSnapshotId\": \"snap_base_image_id\",\n \"scope\": \"\",\n \"project\": \"\",\n \"port\": 7331,\n \"repoUrl\": \"https://host/org/repo.git\",\n \"repoRef\": \"main\",\n \"projectRoot\": \"/abs/path/on/remote/repo\"\n}\n```\n\n---\n\n## 7. Script templates (provider-agnostic shapes)\n\nScaffold under `scripts/orca-vm/`. These are **shapes** — fill in the provider's real commands. All\nreserve stdout for the final JSON and log progress to stderr. Include a shared `json_value ` /\n`env_value ` reader (env → state → fallback) in each.\n\n**Where each script runs:**\n\n- **Local-side** (`create`/`suspend`/`resume`/`destroy` + the base-snapshot/auth scripts the user\n invokes) runs **on the user's desktop**, so it must run on their OS. macOS/Linux: `#!/usr/bin/env\nbash`, `set -euo pipefail`, quoted paths. **Windows:** a bare `.sh` won't run — scaffold `.ps1`/`.cmd`\n or require WSL/Git-Bash and point `orca.yaml` at the right launcher.\n- **Remote-side** (commands you `exec` _inside_ the Linux VM) always runs in the VM's Linux shell, so\n bash is fine there regardless of the user's OS.\n\n### 7a. Base-snapshot (`-base-snapshot.sh`) — Phase 2\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n# resolve base_name/repo_url/repo_ref/project_root/port/scope/project/timeout (env→state→fallback)\n# resolve gh token: GH_TOKEN | GITHUB_TOKEN | `gh auth token`\n# 1. provision a sandbox (timeout/vcpus/published port/snapshot retention); trap: remove on error\n# 2. remote exec (long timeout): install pkgs + gh + corepack/pnpm + agent CLI;\n# clone with GIT_ASKPASS(token); write headless main-only build config;\n# dev setup; pnpm install; build CLI; build headless electron main; smoke-check tools\n# 3. snapshot stopped sandbox; parse snapshot id (fail if unparseable)\n# 4. merge { baseName, snapshotId, projectRoot, repoUrl, repoRef, port, scope, project } into state\n# print only the state JSON to stdout\n```\n\nWorked Vercel commands for this phase are in §7f. You run this script by hand (not via `orca.yaml`),\nafter exporting the first-run inputs the state file doesn't have yet — e.g. provider scope/project, the\nrepo URL/ref, and a git token (`GH_TOKEN`); later runs read them back from state.\n\n### 7b. Auth (`-base-auth.sh`) — Phase 3\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n# read source snapshot from state.snapshotId (fail if absent); auth_name=\"${base_name}-auth\"\n# 1. boot sandbox from source snapshot; trap: remove on error\n# 2. INTERACTIVE/TTY remote exec: agent login — user completes URL/code. Headless VM: MUST use the\n# device-auth flow (e.g. `codex login --device-auth`) — plain OAuth login binds a loopback callback\n# port the host can't reach and hangs. User runs this themselves (you have no interactive TTY); ask\n# them to report back when it's done before continuing.\n# 3. verify login, then refuse to snapshot if not logged in. Prefer the status command's EXIT CODE (most\n# agent CLIs exit non-zero when unauthenticated) over string-matching. If you must grep, fold stderr\n# first (`status 2>&1 | grep …` — many agents print the success line there) and match the agent's exact\n# success line; never `grep -qi 'logged in'`, which also matches \"not logged in\". Codex example: §7f.\n# 4. snapshot; parse new id\n# 5. merge { snapshotId:, authSourceSnapshotId: } into state; remove auth sandbox\n# print only the state JSON to stdout\n```\n\n### 7c. Create (`-create.sh`) — per workspace\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n# read authenticated snapshotId/scope/project/port/repo*/project_root (env→state→fallback)\n# fail clearly if snapshotId is missing (point back to Phases 2–3)\n# name = orca-${ORCA_RECIPE_ID}-${ORCA_VM_INSTANCE_ID} (sanitized, length-capped)\n# 1. boot sandbox from snapshotId with a published port; capture the public URL → pairing address\n# (an externally reachable wss:// URL); trap: remove sandbox on error\n# 2. remote exec: ensure repo at desired commit; rebuild only if commit changed (cache marker)\n# 3. remote exec: start orca serve in the background and read the recipe JSON it writes (see below)\n# 4. print serve's JSON to stdout, optionally enriched with userData:\n# { schemaVersion:1, pairingCode, projectRoot, userData:{ provider, resourceId:name, snapshotId } }\n```\n\n**The exact `orca serve` invocation and its output (verified — do not improvise the flags).** Inside the\nVM, run:\n\n```bash\norca serve \\\n --port \"$PORT\" \\\n --project-root \"$ABS_REPO_PATH_ON_REMOTE\" \\\n --pairing-address \"$EXTERNAL_WSS_URL\" \\\n --recipe-json\n```\n\n**Binary name:** in a VM built from source (the Phase-2 flow), run it as `pnpm exec orca-dev serve …`\nfrom the repo root — `orca-dev` is the in-repo entrypoint and is what the §7f example uses. Plain\n`orca serve …` is the same command when the built CLI is installed on the VM's PATH. The flags/output\nare identical either way.\n\nThere is **no `--host` flag**. `--project-root` must be an absolute directory on the remote. With\n`--recipe-json` the server **stays running** and prints exactly this single object to **stdout**, then\nkeeps serving:\n\n```json\n{\n \"schemaVersion\": 1,\n \"pairingCode\": \"\",\n \"projectRoot\": \"\"\n}\n```\n\n`pairingCode` is the pairing URL, already pointing at whatever you passed as `--pairing-address` — so set\n`--pairing-address` to the externally reachable address and **pass `pairingCode` through unchanged; never\nhand-rewrite it**. Because serve runs in the foreground and doesn't exit, redirect its stdout to a file\nand poll until that file parses as JSON (and bail if the process dies — dump its stderr log). Your\n`create` script then prints that JSON (optionally merging `userData`). Concrete pattern: §7f.\n\n### 7d. Suspend / resume / destroy — per workspace\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\npayload=\"$(cat)\" # Orca passes lifecycle JSON on stdin\nresource_id=\"$(node -e 'const d=JSON.parse(process.argv[1]); process.stdout.write(d.recipeResult?.userData?.resourceId ?? \"\")' \"$payload\")\"\n[ -n \"$resource_id\" ] || { echo \"No resource id in lifecycle payload\" >&2; exit 1; }\n# suspend: provider suspend \"$resource_id\"\n# resume: provider resume \"$resource_id\"; then RE-EMIT fresh recipe JSON (pairing may change)\n# destroy: provider remove \"$resource_id\" (or set destroy: none in orca.yaml)\n```\n\n### 7e. State file — scaffold with scope/project/repo filled in and snapshot ids empty (§6).\n\n### 7f. Worked example — Vercel Sandbox (all three phases)\n\nA real, working shape (the Vercel surface is a CLI: `vercel sandbox create|exec|snapshot|remove`). Adapt\nnames; verify flags against `vercel sandbox --help` for the user's CLI version before relying on them.\nThese ground §7a (base snapshot) and §7b (auth), which are otherwise generic skeletons.\n\n**Phase 2 — base snapshot (§7a):** provision → install tools + clone + headless build → snapshot.\n\n```bash\n# provision a fresh build sandbox (retain a couple of snapshots); trap-remove on error\nvercel sandbox create --name \"$base\" --runtime node24 --timeout 30m --vcpus 4 --publish-port \"$port\" \\\n --snapshot-expiration 30d --keep-last-snapshots 2 \"${vercel_args[@]}\" >&2\n# remote build (long timeout): install pkgs+gh+pnpm+agent CLI, clone with GIT_ASKPASS (write the helper\n# with LITERAL \\$1/\\$GH_TOKEN so they resolve at git-runtime, not write-time — see §5/§7f create — then\n# `rm -f /tmp/askpass.sh`), write the headless main-only build config (drop the renderer), dev setup,\n# build CLI + headless main, smoke-check\nvercel sandbox exec \"$base\" \"${vercel_args[@]}\" --timeout 25m --env \"GH_TOKEN=$gh_token\" … -- bash -lc '…build…' >&2\n# snapshot the STOPPED sandbox and parse the id from CLI output (fail if unparseable)\nout=\"$(vercel sandbox snapshot \"$base\" --stop --expiration 30d \"${vercel_args[@]}\" 2>&1)\"; printf '%s\\n' \"$out\" >&2\nsnapshot_id=\"$(printf '%s\\n' \"$out\" | sed -nE 's/.*(snap_[A-Za-z0-9]+).*/\\1/p' | tail -1)\"\n# merge { baseName, snapshotId, scope, project, port, repoUrl, repoRef, projectRoot } into state; print state JSON\n```\n\n**Phase 3 — agent-auth snapshot (§7b):** boot the base, log the agent in interactively, re-snapshot.\n(`codex` below is an example — substitute the user's chosen agent's login/status verbs, e.g. `claude`.)\n\n```bash\nvercel sandbox create --name \"$auth\" --snapshot \"$snapshot_id\" --timeout 30m --publish-port \"$port\" \"${vercel_args[@]}\" >&2\n# INTERACTIVE — the USER runs this in their own terminal (you have no interactive TTY) and completes the\n# URL/code on the HOST. --device-auth is MANDATORY on a headless VM: plain `codex login` binds a loopback\n# callback port the host browser can't reach and hangs. Ask the user to report back when login finishes.\nvercel sandbox exec --interactive --tty \"$auth\" \"${vercel_args[@]}\" -- bash -lc 'codex login --device-auth'\n# refuse to snapshot an unauthenticated VM — fold stderr, match codex's exact success line (§4)\nvercel sandbox exec \"$auth\" \"${vercel_args[@]}\" --timeout 30s -- bash -lc 'codex login status 2>&1' | grep -Eqi 'Logged in using ChatGPT|Logged in via device' \\\n || { echo \"agent not logged in; not snapshotting\" >&2; exit 1; }\nout=\"$(vercel sandbox snapshot \"$auth\" --stop --expiration 30d \"${vercel_args[@]}\" 2>&1)\"; printf '%s\\n' \"$out\" >&2\nnew_id=\"$(printf '%s\\n' \"$out\" | sed -nE 's/.*(snap_[A-Za-z0-9]+).*/\\1/p' | tail -1)\"\n# overwrite state.snapshotId = new_id, record authSourceSnapshotId = snapshot_id; remove the auth sandbox\n```\n\n**Per-workspace `create`** (the fast path):\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n# resolve from env→state→fallback: snapshot_id, scope, project, port, repo_url, repo_ref, project_root\nvercel_args=(); [ -n \"$scope\" ] && vercel_args+=(--scope \"$scope\"); [ -n \"$project\" ] && vercel_args+=(--project \"$project\")\n[ -n \"$snapshot_id\" ] || { echo \"snapshotId missing — run Phases 2–3 first\" >&2; exit 1; }\ngh_token=\"${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}\"\nrecipe_id=\"${ORCA_RECIPE_ID:-vercel-sandbox}\"\nrecipe_id=\"${recipe_id//./-}\" # Vercel names forbid dots.\ninstance_id=\"${ORCA_VM_INSTANCE_ID:-$(date +%s)}\"\nmax_recipe_id_length=$((128 - ${#instance_id} - 6)) # Preserve the unique instance suffix.\n[ \"$max_recipe_id_length\" -gt 0 ] || { echo \"ORCA_VM_INSTANCE_ID is too long for a Vercel sandbox name\" >&2; exit 1; }\nname=\"orca-${recipe_id:0:max_recipe_id_length}-${instance_id}\"\n\n# Arm cleanup BEFORE create so a failing create can't leak a half-built paid sandbox.\ncleanup_on_error() { [ \"$?\" -ne 0 ] && vercel sandbox remove \"$name\" \"${vercel_args[@]}\" >/dev/null 2>&1 || true; }\ntrap cleanup_on_error EXIT\n\n# 1. boot from the authenticated snapshot, publish the serve port\ncreate_output=\"$(vercel sandbox create --name \"$name\" --snapshot \"$snapshot_id\" \\\n --timeout 30m --publish-port \"$port\" \"${vercel_args[@]}\" 2>&1)\"; printf '%s\\n' \"$create_output\" >&2\n# Vercel prints the published https URL; derive the external wss:// pairing address from it\npublic_url=\"$(printf '%s\\n' \"$create_output\" | sed -nE 's#.*(https://[^[:space:]]+\\.vercel\\.run).*#\\1#p' | head -1)\"\n[ -n \"$public_url\" ] || { echo \"no published URL in create output\" >&2; exit 1; }\npairing_ws=\"${public_url/https:\\/\\//wss://}\"\n\n# 2. (remote) ensure the repo is at the right commit; rebuild only if the commit changed (cache marker)\nvercel sandbox exec \"$name\" \"${vercel_args[@]}\" --timeout 20m \\\n --env \"GH_TOKEN=$gh_token\" --env \"ORCA_PROJECT_ROOT=$project_root\" \\\n --env \"ORCA_REPO_URL=$repo_url\" --env \"ORCA_REPO_REF=$repo_ref\" \\\n -- bash -lc 'set -euo pipefail; cd \"$ORCA_PROJECT_ROOT\"; \\\n # Re-establish git auth for the private-repo fetch (why + full rationale: §5); else it hangs on a prompt.\n # Load-bearing escaping: \\$1 and \\$GH_TOKEN must land LITERALLY and resolve at git-runtime. Test after\n # any edit here — reformatting the nested printf/node quoting silently breaks the fetch or leaks the token.\n if [ -n \"${GH_TOKEN:-}\" ]; then \\\n printf \"%s\\n\" \"#!/usr/bin/env bash\" \"case \\\"\\$1\\\" in *Username*) echo x-access-token;; *Password*) echo \\\"\\$GH_TOKEN\\\";; esac\" > /tmp/askpass.sh; \\\n chmod 700 /tmp/askpass.sh; export GIT_ASKPASS=/tmp/askpass.sh GIT_TERMINAL_PROMPT=0; fi; \\\n git fetch origin \"$ORCA_REPO_REF\"; \\\n git checkout -B \"$ORCA_REPO_REF\" FETCH_HEAD; \\\n rm -f /tmp/askpass.sh; \\\n c=\"$(git rev-parse HEAD)\"; [ -f .orca-built ] && [ \"$(cat .orca-built)\" = \"$c\" ] || { \\\n pnpm install --prefer-offline && pnpm run build:cli && \\\n node config/scripts/run-electron-vite-build.mjs --config config/electron-vite.vm-serve.config.ts && \\\n printf \"%s\" \"$c\" > .orca-built; }' >&2\n\n# 3. (remote) start orca serve in the background, writing recipe JSON to a file; poll until it parses\nrecipe_json=\"$(vercel sandbox exec \"$name\" \"${vercel_args[@]}\" --timeout 60s \\\n --env \"ORCA_PORT=$port\" --env \"ORCA_PROJECT_ROOT=$project_root\" --env \"ORCA_PAIRING_ADDRESS=$pairing_ws\" \\\n -- bash -lc 'set -euo pipefail; cd \"$ORCA_PROJECT_ROOT\"; rm -f /tmp/orca-recipe.json /tmp/orca-serve.log; \\\n nohup pnpm exec orca-dev serve --port \"$ORCA_PORT\" --project-root \"$ORCA_PROJECT_ROOT\" \\\n --pairing-address \"$ORCA_PAIRING_ADDRESS\" --recipe-json >/tmp/orca-recipe.json 2>/tmp/orca-serve.log /dev/null 2>&1 && { cat /tmp/orca-recipe.json; exit 0; }; \\\n kill -0 \"$pid\" 2>/dev/null || { cat /tmp/orca-serve.log >&2; exit 1; }; sleep 0.25; \\\n done; cat /tmp/orca-serve.log >&2; echo \"serve recipe JSON timed out\" >&2; exit 1')\"\n\n# 4. print serve's JSON enriched with userData (single object on stdout)\nnode -e 'const p=JSON.parse(process.argv[1]); console.log(JSON.stringify({...p, schemaVersion:1,\n userData:{...p.userData, provider:\"vercel-sandbox\", resourceId:process.argv[2], snapshotId:process.argv[3]}}))' \\\n \"$recipe_json\" \"$name\" \"$snapshot_id\"\ntrap - EXIT\n```\n\n`suspend`/`resume`/`destroy` use `vercel sandbox stop|...|remove \"$resource_id\"` reading\n`userData.resourceId` from stdin (§7d). This is the **Orca-server** connection mode (the recipe emits a\npairing URL). If the user chose **SSH** in the §1 interview, use §7g instead.\n\n### 7g. Worked example — existing SSH host (SSH connection mode)\n\nSSH mode is **fundamentally different from §7c/§7f**, not a relabeling of them:\n\n- **`create` does NOT run `orca serve` and does NOT emit a `pairingCode`.** Orca itself connects to the\n host over its SSH relay, brings up the git + filesystem providers, and imports the repo. The script's\n only job is to make the host ready and **print SSH connection details** Orca will dial.\n- The result uses a `connection` block with `type: \"ssh\"` and a `target`, **not** the flat\n `pairingCode`/`projectRoot` shape. Exact shape (Orca rejects anything else):\n\n```json\n{\n \"schemaVersion\": 1,\n \"connection\": {\n \"type\": \"ssh\",\n \"projectRoot\": \"/abs/path/to/repo/on/host\",\n \"target\": {\n \"label\": \"my-box\",\n \"host\": \"192.0.2.10\",\n \"port\": 22,\n \"username\": \"ubuntu\",\n \"identityFile\": \"~/.ssh/id_ed25519\",\n \"jumpHost\": \"bastion.example.com\",\n \"proxyCommand\": \"cloudflared access ssh --hostname %h\",\n \"relayGracePeriodSeconds\": 0,\n \"portForwards\": []\n }\n }\n}\n```\n\n`label`, `host`, `port`, `username` are required; the rest are optional — omit any you don't need.\n\nFor an explicitly requested one-VM-per-workspace checkout, the create script must read\n`ORCA_RECIPE_RESULT_SCHEMA_VERSION`, `ORCA_REPO_URL`, `ORCA_REPO_REF`, `ORCA_REPO_REF_HEAD`, and\n`ORCA_REPO_BRANCH`. Use `ORCA_REPO_REF` to fetch the selected source, but create\n`ORCA_REPO_BRANCH` at the exact `ORCA_REPO_REF_HEAD` commit; resolving the symbolic ref again can race\nwith an upstream update. `ORCA_REPO_URL` and `ORCA_REPO_REF` are a matched fetch pair, including when\nthe desktop source uses multiple remotes. Return that primary checkout at `projectRoot` and emit the\nsame SSH result with:\n\n```bash\n[ -n \"${ORCA_REPO_REF_HEAD:-}\" ] || { echo \"missing pinned source commit\" >&2; exit 1; }\ngit fetch origin \"$ORCA_REPO_REF\"\ngit cat-file -e \"${ORCA_REPO_REF_HEAD}^{commit}\"\ngit checkout -B \"$ORCA_REPO_BRANCH\" \"$ORCA_REPO_REF_HEAD\"\n```\n\n```json\n{\n \"schemaVersion\": 2,\n \"checkoutMode\": \"provisioned-root\",\n \"connection\": {\n \"type\": \"ssh\",\n \"projectRoot\": \"/abs/repo\",\n \"target\": { \"label\": \"my-box\", \"host\": \"192.0.2.10\", \"port\": 22, \"username\": \"ubuntu\" }\n }\n}\n```\n\nFail if the requested schema is not `2`; do not silently fall back to the ordinary recipe shape.\n\n**Networking → which `target` fields to set** (how _your desktop_ reaches the box — there is no\n`orca serve` URL in SSH mode):\n\n- Public IP / DNS, or a Tailscale/VPN address → `host`; SSH port → `port` (usually 22).\n- Key auth → `identityFile` (add `identitiesOnly: true` if the agent has many keys).\n- Through a bastion → `jumpHost` (a `user@host` ProxyJump) **or** a full `proxyCommand` (e.g. an access\n proxy). Use one, not both.\n- A service port the workspace needs → add entries to `portForwards`.\n- `relayGracePeriodSeconds` (optional): how long Orca keeps the SSH relay alive after the workspace\n detaches before tearing it down; `0` = tear down immediately. Leave it off unless the user wants a\n reconnect grace window.\n\n**Toolchain & agent auth on a persistent (no-snapshot) host — do this ONCE, by hand, before wiring the\nrecipe** (there's no base image to bake; the host _is_ the base). Run the §7f Phase-2 install steps and\nthe §7f Phase-3 ` login --device-auth` **directly over SSH on the host** (interactive, e.g.\n`ssh -t user@host ' login --device-auth'`). After that the host stays ready across workspaces.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n# resolve from env→state→fallback (default unset optionals to \"\"): ssh_username, host,\n# ssh_port (default 22), identity_file, jump_host, proxy_command, project_root, repo_url, repo_ref\n: \"${identity_file:=}\"; : \"${jump_host:=}\"; : \"${proxy_command:=}\" # avoid set -u aborts on optionals\ngh_token=\"${GH_TOKEN:-${GITHUB_TOKEN:-$(command -v gh >/dev/null 2>&1 && gh auth token 2>/dev/null || true)}}\"\nssh_target=\"${ssh_username}@${host}\"\nssh_opts=(-p \"$ssh_port\"); [ -n \"$identity_file\" ] && ssh_opts+=(-i \"$identity_file\")\n# Why: a fresh host's key isn't in known_hosts; a StrictHostKeyChecking prompt would HANG a\n# non-interactive create. Pre-add the key (or set the option) so it can't block.\nssh-keyscan -p \"$ssh_port\" \"$host\" >> \"$HOME/.ssh/known_hosts\" 2>/dev/null || true\n\n# 1. ensure the repo is present and at the right commit on the host (NO orca serve here)\nssh \"${ssh_opts[@]}\" \"$ssh_target\" \\\n \"GH_TOKEN='$gh_token' GIT_TERMINAL_PROMPT=0 bash -lc '\n set -euo pipefail\n [ -d \\\"$project_root/.git\\\" ] || git clone \\\"$repo_url\\\" \\\"$project_root\\\"\n cd \\\"$project_root\\\" && git fetch origin \\\"$repo_ref\\\" && git checkout -B \\\"$repo_ref\\\" FETCH_HEAD\n '\" >&2\n\n# 2. print the SSH connection block (NO pairingCode, NO orca serve). host/port/username tell Orca's\n# relay how to dial in; identityFile/jumpHost/proxyCommand/portForwards are emitted when set.\nnode -e 'const [host,port,user,idf,jh,pc,root]=process.argv.slice(1);\n const target={ label:\"per-workspace-host\", host, port:Number(port), username:user };\n if(idf) target.identityFile=idf; if(jh) target.jumpHost=jh; if(pc) target.proxyCommand=pc;\n // add target.portForwards=[...] here if the workspace needs forwarded service ports\n console.log(JSON.stringify({ schemaVersion:1, connection:{ type:\"ssh\", projectRoot:root, target } }))' \\\n \"$host\" \"$ssh_port\" \"$ssh_username\" \"$identity_file\" \"$jump_host\" \"$proxy_command\" \"$project_root\"\n```\n\n`suspend`/`resume`/`destroy`: on a persistent host there's usually nothing to tear down — set\n`destroy: none` and omit suspend/resume. (Orca still disconnects/reconnects its own SSH relay on\nsleep/wake/delete — that's separate from these scripts.)\n\nIf the SSH host is instead an **ephemeral/snapshot-capable VM** (your hypervisor, or a cloud VM with\nimage support), keep the §7f Phase-2/3 base-image model for provisioning, but still emit the\n`connection.type:\"ssh\"` block above instead of starting `orca serve`.\n\n### 7h. Worked example — local Docker SSH (SSH connection mode)\n\nLocal Docker can model an ephemeral SSH VM without cloud cost: build a base image with `sshd`, tools,\nrepo prerequisites, and the agent CLI; run an **interactive auth container** once; then `docker commit`\nthat container as the authenticated image used by per-workspace `create`.\n\nKey points:\n\n- Publish container SSH to a random localhost port (`-p 127.0.0.1::22`) and emit\n `connection.type:\"ssh\"` with `host:\"127.0.0.1\"`, that port, `username`, `identityFile`, and\n `identitiesOnly:true`.\n- Generate a repo-local SSH key if needed, but gitignore the private/public key files.\n- **Bake SSH host keys into the base image** (`ssh-keygen -A` at **build** time; at runtime only generate\n if absent). Ephemeral containers all present the **same** host key, so `known_hosts` on `127.0.0.1`\n doesn't churn as the published port rotates across workspaces (otherwise every container's freshly\n generated key collides on `localhost` and trips host-key-changed warnings).\n- The auth image is the Docker equivalent of Phase 3: the **user** runs the agent login **inside** the\n container (you can't drive it — you have no interactive TTY), configures proxy env/config, approves\n hooks, and you commit once they report it's done. On a headless container use the **device-auth** flow\n (§4). Verify login before committing — exit code, or fold stderr and match the exact success line (§4).\n- Do not bind-mount or copy the host's full agent home into the image. Let each container have writable\n agent state; only the committed auth image should carry reusable authenticated state.\n- If committing from an interactive shell, force the runtime entrypoint back to `sshd`:\n `docker commit --change='ENTRYPOINT [\"/usr/local/bin/orca-docker-ssh-entrypoint\"]' …`.\n- `destroy` should read `recipeResult.userData.resourceId` and run `docker rm -f \"$resource_id\"`.\n\nValidation before wiring/live use:\n\n```bash\ndocker image inspect \"$auth_image\" --format '{{json .Config.Entrypoint}}'\ndocker run -d --name \"$name\" -p 127.0.0.1::22 -e \"ORCA_SSH_PUBLIC_KEY=$pubkey\" \"$auth_image\"\ndocker ps -a --filter \"name=$name\"\ndocker logs \"$name\"\nssh -i \"$key\" -p \"$port\" -o IdentitiesOnly=yes user@127.0.0.1 'codex --version'\n```\n\nIf the container exits immediately, inspect logs before the cleanup trap removes it; a committed\ninteractive image with `ENTRYPOINT [\"bash\"]` is a common cause.\n\nAlso confirm the **host key is stable** across containers: the SSH `ssh -i … 127.0.0.1` dial should not\ntrigger a host-key-changed warning when a second container reuses the port. If it does, the host keys\nweren't baked into the base image (see the `ssh-keygen -A` point above).\n\n### 7i. Windows local-side scripts\n\nThe local-side scripts run on the user's desktop. On **Windows**, a bare `.sh` won't execute. Either\nrequire WSL/Git-Bash (and point `orca.yaml` at e.g. `bash ./scripts/orca-vm/.sh` via a `.cmd`\nlauncher), or scaffold PowerShell equivalents. Minimal PowerShell shape:\n\n```powershell\n#requires -Version 5\n$ErrorActionPreference = 'Stop'\n# resolve env→state→fallback; run the provider CLI / ssh the same way;\n# capture provider output; build the result object for the chosen mode and write ONE line of JSON to stdout.\n# Orca-server mode: @{ schemaVersion=1; pairingCode=$pairingCode; projectRoot=$projectRoot; userData=@{...} }\n# SSH mode: @{ schemaVersion=1; connection=@{ type=\"ssh\"; projectRoot=$projectRoot;\n# target=@{ label=$label; host=$host; port=$port; username=$user } } } (see §7g/§7h)\n($result | ConvertTo-Json -Compress -Depth 6)\n# progress/errors → Write-Error / the error stream, never stdout.\n```\n\nThe remote-side commands you run _inside_ the Linux VM stay bash regardless of the desktop OS.\n\n---\n\n## 8. Per-workspace recipe contract (the fast path)\n\nOnce the authenticated snapshot exists, this runs on every workspace create. Define recipes in\n`orca.yaml`:\n\n```yaml\nenvironmentRecipes:\n - id: cloud-sandbox\n name: Cloud Sandbox\n create: ./scripts/orca-vm/cloud-sandbox-create.sh\n suspend: ./scripts/orca-vm/cloud-sandbox-suspend.sh\n resume: ./scripts/orca-vm/cloud-sandbox-resume.sh\n destroy: ./scripts/orca-vm/cloud-sandbox-destroy.sh\n```\n\n`create` runs **locally from the repo root** and prints **one** JSON object to stdout. Its shape depends\non the connection mode chosen in §1:\n\n**Orca-server mode** — boot the env, start `orca serve` in it, and print serve's result:\n\n```json\n{\n \"schemaVersion\": 1,\n \"pairingCode\": \"orca-pairing-code-or-url\",\n \"projectRoot\": \"/absolute/path/to/repo/on/remote\",\n \"userData\": { \"provider\": \"example\", \"resourceId\": \"provider-resource-id\" }\n}\n```\n\nHere `pairingCode` (from `orca serve --recipe-json`) and `projectRoot` are required; `schemaVersion` (`1`)\nand `userData` are optional.\n\n**SSH mode** — do **not** run `orca serve`; print the `connection.type:\"ssh\"` block instead (full shape +\nworked script in §7g). `pairingCode` is **not** used in SSH mode.\n\n**Optional provisioned root** — only for direct SSH and only when explicitly requested. Add\n`checkoutMode: provisioned-root` to the recipe, require `ORCA_RECIPE_RESULT_SCHEMA_VERSION=2`, create\nthe requested `ORCA_REPO_BRANCH` at the pinned `ORCA_REPO_REF_HEAD` commit (use `ORCA_REPO_REF` only\nto fetch that commit) at the returned `projectRoot`, and emit schema version 2 with\n`checkoutMode: \"provisioned-root\"`. All recipes without this field retain the schema-v1 behavior above.\n\nLifecycle hooks (all run locally):\n\n- `create`: required. Prints recipe result JSON.\n- `suspend`: optional. Sleep; reads lifecycle payload on stdin.\n- `resume`: optional. Wake; reads payload on stdin and **prints fresh recipe JSON** (pairing may change).\n- `destroy`: optional unless `destroy: none`. Delete/cleanup; reads payload on stdin.\n\nStart Orca remotely with `orca serve --port \"$PORT\" --project-root \"$ABS_ROOT\" --pairing-address\n\"$EXTERNAL_WSS_URL\" --recipe-json` (exact flags + output in §7c). Set `--pairing-address` to the\nexternally reachable address so the emitted `pairingCode` is reachable; tunneling/port mapping is the\nscript's job.\n\nBackward compatibility: `command`→`create`, `cleanup`→`destroy`, `cleanup: none`→`destroy: none`.\nPrefer the lifecycle names.\n\n---\n\n## 9. Doctor and validation\n\nValidate in two stages — the cheap dry run first, then the live self-test.\n\n### Dry run (free, non-destructive) — always do this first\n\n`orca vm recipe doctor --repo-path --json` validates **static wiring only** — it does\n**not** boot anything. It checks: local-host execution (v1), repo path, recipe id exists,\ncreate/destroy/suspend/resume command paths resolve, suspend/resume are paired, and each script is\nexecutable (POSIX exec bit; skipped on Windows). Fix every failure here before spending any cloud money.\n\n### Live self-test (`--provision`) — diagnose and iterate yourself\n\n`orca vm recipe doctor --repo-path --provision --json` actually runs the recipe end\nto end: it executes `create`, validates the returned recipe JSON, then runs `destroy` to **tear the\nenvironment back down** (so the test leaves nothing running, as long as `destroy` works). It spends real\ncloud money, so get the user's OK **once** before starting — that one approval covers the whole loop\nbelow; do not re-ask before each run.\n\nOn failure, the JSON result includes a `provisionTranscript` with the **complete** captured output of\neach stage so you can self-diagnose without asking the user to relay logs:\n\n```json\n{\n \"ok\": false,\n \"checks\": [{ \"id\": \"recipe.provision\", \"status\": \"fail\", \"message\": \"…\" }],\n \"provisionTranscript\": {\n \"provision\": { \"exitCode\": 0, \"signal\": null, \"stdout\": \"…\", \"stderr\": \"…\", \"parseError\": \"…\" },\n \"destroy\": { \"exitCode\": 0, \"signal\": null, \"stdout\": \"…\", \"stderr\": \"…\" }\n }\n}\n```\n\n**Run it as a loop:** read `provisionTranscript.provision.stderr` / `.stdout` / `.parseError` (and\n`destroy.*`), fix the script, and re-run `--provision` until `ok` is `true` — iterating on your own\nrather than waiting for the user to paste errors. Common reads: a non-empty `stderr` with `exitCode 0`\nplus a `parseError` means `create` ran but printed something other than the single recipe-result JSON on\nstdout (often a stray `echo` — route it to stderr, see §10); a non-zero `exitCode` is a provider/script\nfailure described in `stderr`. Each stream is redacted and capped (head+tail) — large logs keep both the\nsetup context and the failure.\n\nThe self-test cannot see provider-side truth beyond what the scripts print, so still confirm: state has a\npopulated **authenticated** `snapshotId` (Phases 2–3 done), and `destroy` is implemented/tested (or\nexplicitly `none` — in which case the self-test won't tear down, so clean up manually).\n\nFor SSH recipes, also smoke-test the exact emitted target before declaring success: dial the host/port\nwith the identity/proxy settings, run `pwd`, verify the repo path, check the agent binary, and confirm\n`destroy` removes the provider resource/container. For Docker, inspect the auth image entrypoint and do a\nstartup-only `docker run` before the full clone/install path.\n\n---\n\n## 10. Failure modes\n\n- **Build exceeds plan timeout (e.g. Hobby 45m).** Use enough vCPUs and a timeout covering the build;\n else split work or use a higher plan. The cap also limits per-workspace runtime — surface it.\n- **Build exceeds plan RAM.** Build the **headless main only** (drop the renderer) — the biggest fitter.\n- **Private-repo clone hangs/fails.** Wrong/missing token. Use `GIT_ASKPASS` + `GIT_TERMINAL_PROMPT=0`\n so it fails fast instead of prompting.\n- **`GIT_ASKPASS` helper aborts the clone with \"`$1: unbound variable`\".** The `printf`/heredoc that writes\n the helper inside `bash -lc` under `set -u` expanded `$1`/`$GH_TOKEN` at **write** time. Escape them\n (`\\$1`, `\\$GH_TOKEN`) so they land literally and resolve at git-runtime; this also keeps the real token\n out of the file. `rm -f` the helper afterward (§5, §7f).\n- **Agent verified as \"not logged in\" despite a good login.** `codex login status` (and similar) print\n \"Logged in …\" to **stderr**; an stdout-only `grep` misses it. Prefer the status **exit code**; if you\n grep, fold stderr first (`status 2>&1 | grep …`) and match the exact success line — not `grep -qi\n'logged in'`, which also matches \"not logged in\".\n- **Headless agent login hangs.** Plain OAuth `login` starts a loopback callback server on a VM/container\n port the host browser can't reach. Use the **device-auth** flow (`login --device-auth`) — it prints a\n URL + code the user opens on the host.\n- **`known_hosts` host-key churn on local Docker.** Each ephemeral container regenerating its SSH host key\n collides on `127.0.0.1` as the published port rotates. Bake host keys into the base image at build time\n (`ssh-keygen -A`; runtime generates only if absent) so all containers share one stable key (§7h).\n- **Snapshot expired/evicted.** If `create` hits an unknown snapshot id, rerun Phases 2–3 and update\n `snapshotId`.\n- **Agent auth didn't persist.** Confirm `snapshotId` points at the **authenticated** snapshot; re-run\n Phase 3. Warn that short-lived tokens may need periodic re-auth.\n- **Agent auth copied from the host breaks.** Do not bind-mount/copy a full host agent home; sqlite\n files can be unwritable or host-specific, hooks may need approval again, and config may reference\n local-only env vars. Authenticate inside the runtime and snapshot/commit that layer.\n- **Docker auth image exits immediately.** Inspect `docker image inspect … .Config.Entrypoint` and\n `docker logs`. If the image was committed from an interactive shell, reset the entrypoint to the SSH\n entrypoint during `docker commit`.\n- **Leaked paid resource.** Every long script must trap errors and remove the sandbox it created.\n- **`create` emits non-JSON on stdout.** A stray `echo` corrupts the result — stdout is for the final\n JSON only; everything else to stderr. The `--provision` self-test surfaces this as `exitCode 0` + a\n `parseError` with the offending stdout in `provisionTranscript` (§9).\n\n---\n\n## 11. Boundaries\n\n- Don't create accounts, choose plans/regions, or invent scope/project/org/image/billing ids.\n- Don't invent or store credentials; no secrets in `userData`, state, comments, docs, or commits.\n- Don't run paid/long phases (base snapshot, auth, live test) without an explicit OK.\n- Don't hide provider errors behind generic messages — preserve actionable stderr.\n- Don't make Orca own provider lifecycle beyond invoking the configured scripts.\n- Don't commit or create an Orca workspace unless asked.\n" // oxfmt-ignore -const ORCHESTRATION_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers with durable Runs, Tasks, Dispatches,\n messages, questions, gates, and completion tracking. Use when the user asks\n to supervise, monitor, wait for results, coordinate a DAG, or manage blocking\n agent-to-agent questions. For full ownership handoffs or ordinary terminal,\n worktree, and built-in-browser control, use `orca-cli`. Use Computer Use for\n external browser windows, webviews, Orca app UI, or desktop UI outside Orca's\n embedded browser only when the task requires OS/window-level control such as\n focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for Orca's\n embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision.\n\n**Done:** all expected Dispatches have settled, every delivered message was\nprocessed before acknowledgment, and each settled worker was reused, explicitly\nretained, or released.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. A timeout, quiet terminal, missing client, or lost remote\nconnection is never proof of failure or exit.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested. Before\nclaiming a worker was orchestrated, verify its Task and Dispatch exist.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Folder workspaces are valid. Do not require Git or assume every workspace is a\n worktree.\n- When a command requires an exact worktree selector, use the full\n `::` value returned by Orca; a bare repo id is not a worktree id.\n- For a newly created workspace, pass that returned value as `id:`;\n do not shorten it to the repository id.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable selected by the discovery stub for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- Legacy takeover binds the authenticated invoking terminal; `--from` cannot\n nominate another coordinator. It preserves live work and fences the former\n coordinator, so never take over while that coordinator is still active.\n- A successful `orchestration send` proves durable enqueue. Its wake or nudge is\n best-effort attention only; it does not prove the recipient read the message,\n started a turn, or accepted steering.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n4. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nThe generic shape below is only a reminder. Copy the live preamble's command,\nincluding its executable, `--from`, and `--dispatch-capability` values:\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs. A\ncoordinator-supervised follow-up arrives with a fresh preamble and Task block.\n\n## Canonical supervised loop\n\nConfirm the runtime, create or bind one Run, create all independent Tasks, and\nstart the full independent wave before waiting:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration task-create --spec \"\" --json\nORCA orchestration task-create --spec \"\" --json\nORCA orchestration worker-start --task --worktree current --agent codex --json\nORCA orchestration worker-start --task --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nUse Task dependencies only for real ordering. Prefer parallel waves over chains\ndeeper than three or four steps. Nested workers obey the configured depth limit;\ncreating another Run does not reset the caller's depth.\n\nA consuming `check` returns the bound Run's oldest FIFO Delivery and replays that\nbatch until acknowledged. Process every message. Reply to questions, validate\nthat each `worker_done` belongs to the expected active Dispatch, and decide each\nsettled terminal's next owner before acknowledging:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor from timeout, idle state, heartbeat, relay loss, or missing client alone.\n\n`worker-start` is the normal path. It composes placement, terminal readiness,\nprompt injection, and supervised resource ownership. Low-level\n`dispatch --inject` is reserved for an expressiveness gap and leaves an\noperator-created process unsupervised.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Never release because of\nidle state, timeout, heartbeat, status, question, escalation, or a rejected or\nstale completion. If release is uncertain, follow its exact recovery receipt;\nnever substitute `terminal close`. Released output remains readable through\n`worker-read`.\n\nA valid `worker_done` settles the Task and Dispatch automatically. Do not follow\nit with `task-update --status completed`. Do not end the coordinator turn until\nall expected Dispatches and settled terminals are accounted for.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --full` once and read only the named\nbundled reference. `--full` returns this exact kernel and every reference from\nthe same CLI build in one deterministic document. If an older CLI rejects\n`--full`, keep this kernel's safety floor and use that command's `--help`; do not\nguess newer flags.\n\n| Action gate | Bundled reference |\n| ----------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| Worker ask/resume, heartbeat, escalation, or completion command details | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n" +const ORCHESTRATION_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers with durable Runs, Tasks, Dispatches,\n messages, questions, gates, and completion tracking. Use when the user asks\n to supervise, monitor, wait for results, coordinate a DAG, or manage blocking\n agent-to-agent questions. For full ownership handoffs or ordinary terminal,\n worktree, and built-in-browser control, use `orca-cli`. Use Computer Use for\n external browser windows, webviews, Orca app UI, or desktop UI outside Orca's\n embedded browser only when the task requires OS/window-level control such as\n focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for Orca's\n embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Done:** all expected Dispatches\nhave settled, every delivered message was processed before acknowledgment, and\neach settled worker was reused, explicitly retained, or released.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. A timeout, quiet terminal, missing client, or lost remote\nconnection is never proof of failure or exit.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested. Before\nclaiming a worker was orchestrated, verify its Task and Dispatch exist.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid. Do not require Git or assume every workspace is a\n worktree.\n- Worktree selectors need the full `::` value Orca returned,\n passed as `id:`; a bare repo id is not a worktree id.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable selected by the discovery stub for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- Legacy takeover binds the authenticated invoking terminal; `--from` cannot\n nominate another coordinator. Never take over while that coordinator is active.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nThe generic shape below is only a reminder. Copy the live preamble's command,\nincluding its executable, `--from`, and `--dispatch-capability` values:\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs. A\ncoordinator-supervised follow-up arrives with a fresh preamble and Task block.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor from timeout, idle state, heartbeat, relay loss, or missing client alone.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --json`. Act on each row's `attention`,\n`requiresAction`, and literal `nextAction` argv. A worker that is not `live`,\nwhose `worker-show` reports `agentWait` null, and whose `worker-read` shows no\nnew progress is stalled, not working: load `references/recovery-and-cleanup.md`\nand choose `worker-stop` or `worker-abandon` explicitly.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Never release because of\nidle state, timeout, heartbeat, status, question, escalation, or a rejected or\nstale completion. If release is uncertain, follow its exact recovery receipt and\nnever substitute `terminal close`. Released output stays readable via\n`worker-read`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --terminal-state reclaimable --json`, and do not end\nthe coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --full` once and read only the named\nbundled reference: it returns this exact kernel and every reference from the same\nCLI build. If an older CLI rejects `--full`, keep this kernel's safety floor, use\nthat command's `--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ----------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| Worker ask/resume, heartbeat, escalation, or completion command details | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n" // oxfmt-ignore -const ORCHESTRATION_FULL_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers with durable Runs, Tasks, Dispatches,\n messages, questions, gates, and completion tracking. Use when the user asks\n to supervise, monitor, wait for results, coordinate a DAG, or manage blocking\n agent-to-agent questions. For full ownership handoffs or ordinary terminal,\n worktree, and built-in-browser control, use `orca-cli`. Use Computer Use for\n external browser windows, webviews, Orca app UI, or desktop UI outside Orca's\n embedded browser only when the task requires OS/window-level control such as\n focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for Orca's\n embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision.\n\n**Done:** all expected Dispatches have settled, every delivered message was\nprocessed before acknowledgment, and each settled worker was reused, explicitly\nretained, or released.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. A timeout, quiet terminal, missing client, or lost remote\nconnection is never proof of failure or exit.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested. Before\nclaiming a worker was orchestrated, verify its Task and Dispatch exist.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Folder workspaces are valid. Do not require Git or assume every workspace is a\n worktree.\n- When a command requires an exact worktree selector, use the full\n `::` value returned by Orca; a bare repo id is not a worktree id.\n- For a newly created workspace, pass that returned value as `id:`;\n do not shorten it to the repository id.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable selected by the discovery stub for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- Legacy takeover binds the authenticated invoking terminal; `--from` cannot\n nominate another coordinator. It preserves live work and fences the former\n coordinator, so never take over while that coordinator is still active.\n- A successful `orchestration send` proves durable enqueue. Its wake or nudge is\n best-effort attention only; it does not prove the recipient read the message,\n started a turn, or accepted steering.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n4. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nThe generic shape below is only a reminder. Copy the live preamble's command,\nincluding its executable, `--from`, and `--dispatch-capability` values:\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs. A\ncoordinator-supervised follow-up arrives with a fresh preamble and Task block.\n\n## Canonical supervised loop\n\nConfirm the runtime, create or bind one Run, create all independent Tasks, and\nstart the full independent wave before waiting:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration task-create --spec \"\" --json\nORCA orchestration task-create --spec \"\" --json\nORCA orchestration worker-start --task --worktree current --agent codex --json\nORCA orchestration worker-start --task --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nUse Task dependencies only for real ordering. Prefer parallel waves over chains\ndeeper than three or four steps. Nested workers obey the configured depth limit;\ncreating another Run does not reset the caller's depth.\n\nA consuming `check` returns the bound Run's oldest FIFO Delivery and replays that\nbatch until acknowledged. Process every message. Reply to questions, validate\nthat each `worker_done` belongs to the expected active Dispatch, and decide each\nsettled terminal's next owner before acknowledging:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor from timeout, idle state, heartbeat, relay loss, or missing client alone.\n\n`worker-start` is the normal path. It composes placement, terminal readiness,\nprompt injection, and supervised resource ownership. Low-level\n`dispatch --inject` is reserved for an expressiveness gap and leaves an\noperator-created process unsupervised.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Never release because of\nidle state, timeout, heartbeat, status, question, escalation, or a rejected or\nstale completion. If release is uncertain, follow its exact recovery receipt;\nnever substitute `terminal close`. Released output remains readable through\n`worker-read`.\n\nA valid `worker_done` settles the Task and Dispatch automatically. Do not follow\nit with `task-update --status completed`. Do not end the coordinator turn until\nall expected Dispatches and settled terminals are accounted for.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --full` once and read only the named\nbundled reference. `--full` returns this exact kernel and every reference from\nthe same CLI build in one deterministic document. If an older CLI rejects\n`--full`, keep this kernel's safety floor and use that command's `--help`; do not\nguess newer flags.\n\n| Action gate | Bundled reference |\n| ----------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| Worker ask/resume, heartbeat, escalation, or completion command details | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n\n---\n\n# Bundled references\n\nThese references belong to the version-matched guide above. Read only the documents named by its action gates.\n\n\n\n# Coordinator loop\n\nLoad this reference for expanded DAG waves, per-invocation launch preferences,\nsame-terminal reuse, or review ownership. The compact guide remains the source\nof truth for the loop order and completion boundary.\n\n## Ready waves\n\nCreate independent Tasks before the first wait. Encode only real dependencies,\nthen use the ready view as external memory:\n\n```text\nORCA orchestration task-create --spec \"\" --deps --json\nORCA orchestration task-list --ready --brief --json\n```\n\n`--brief` collapses whitespace and caps echoed specs at 160 characters;\n`spec_truncated` identifies shortened rows. Omit it when full specs are needed or\nwhen an older CLI rejects the flag. A nested worker must respect\n`nested_worker_depth_exceeded`; creating another Run does not reset depth.\n\n## Launch preferences\n\nFor a fresh Claude, Codex, or Cursor terminal, `--model` accepts an opaque\nprovider model ID. Add `--effort` only when that model supports it:\n\n```text\nORCA orchestration worker-start --task --worktree current --agent claude --model opus --effort high --json\n```\n\n`--effort` requires `--model`; neither option combines with `--terminal`. A\nconnected worker server must advertise launch-preference support before Orca\nforwards either field. Compare `launch.requested` with `launch.effective`; never\nclaim a model or effort from requested arguments alone.\n\n## Reuse after settlement\n\nChoose the terminal's next owner before acknowledging the Delivery. When the\nsame exact agent has immediate follow-up work, recover the proven handle and\ntransfer cleanup ownership to the new Dispatch:\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-start --task --terminal --json\n```\n\nOtherwise explicitly retain or release the settled worker. Do not leave it live\nonly to inspect output; archived output remains available through `worker-read`.\n\n## Review ownership\n\nA review-only `worker_done` authorizes synthesis of findings, not coordinator\nfile edits. Dispatch or hand off fixes unless the user explicitly assigned them\nto the coordinator. If the user's plan names a next owner, post-review fixes and\nPR preparation remain with that owner; the coordinator routes and synthesizes.\n\n\n\n# Legacy contract migration\n\nLoad this reference only for an authority label, adopted Run, compatibility or\nrecovery receipt, or explicit legacy takeover. A newly created attempt always\nuses the current grammar.\n\n## Authority labels\n\n- `[LEGACY COMPATIBILITY]` is live and attested. Run only the exact supported\n command printed with the message, using the same selected executable and\n arguments supplied by the original prompt.\n- `[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]` is one bounded,\n at-least-once cutover replay. Process it idempotently and acknowledge only\n through the exact displayed guidance.\n- `[LEGACY READ-ONLY]` is inspection-only. It has no reply, acknowledgment, or\n lifecycle mutation.\n- An unlabeled current message uses the current guide and grammar.\n\nAn explicitly selected current Run, attested current binding, current Dispatch,\nor federated attachment takes precedence over legacy fallback. A retained\nadoption record alone does not grant mutation authority. If liveness, principal\nownership, capability, or the exact legacy contract is unproven, degrade to\nread-only inspection and never fall back to local execution.\n\nAdoption preserves the live agent process, PTY/session, terminal handle,\ntab/pane, worktree or folder workspace, Task, and Dispatch. It never restarts or\nreplaces the worker and never revives the retired scheduler. Loss of lifecycle\nauthority does not invalidate the existing process, assignment, or filesystem\nwork. Exact recovery may restore the same PTY once in its original inactive\nbackground tab; it must not spawn, write, signal, stop, switch, focus, split, or\ninject a terminal.\n\n## Compatibility recovery\n\nWhen a compatibility response returns structured next-step arguments, execute\nthose exact arguments with the same selected CLI executable. Do not translate\nfrom memory, broaden the recipient, or retry as a current mutation unless the\nreceipt explicitly authorizes it.\n\nA pending ask, reply, final Dispatch settlement, and consuming check have\ndurable recovery identities. Heartbeat and escalation remain at-least-once\nacross a manual contract-boundary retry. If an ask may already have been\nanswered, run the exact non-consuming recovery check printed by Orca before\ncreating any new question. Never guess among identical question threads.\n\nOn packaged Windows, a legacy ask uses a two-step commit/resume protocol. The\ninitial command commits the question, prints its exact\n`ask --resume ` command, and exits with launcher status `75`. Run\nthat exact resume after the launcher or update boundary. For an attested WSL\nlaunch, preserve the printed `orca-ide` executable and distro route. Older WSL\nworkers without launch proof remain lifecycle read-only even while their\nterminal and filesystem work continue.\n\n## Read-only inspection and takeover\n\nRead-only inspection does not consume mail:\n\n```text\nORCA orchestration run-list --json\nORCA orchestration run-show --id run_legacy_local --json\nORCA orchestration run-show --id --json\nORCA orchestration task-list --run --json\nORCA orchestration inbox --full --json\nORCA orchestration check --terminal --peek --format --json\nORCA terminal read --terminal --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\n```\n\n`run_legacy_local` is an empty audit tombstone after adoption. Find the ordinary\nRun whose objective is `Recovered orchestration work from a contract update`.\n\nOnly when the original coordinator is unavailable or cannot prove retained\nauthority may a new live coordinator take over from its own terminal:\n\n```text\nORCA orchestration run-use --id --takeover-legacy --json\nORCA orchestration check --run --json\n```\n\nTakeover fences only the old coordinator and moves pending mail into current Run\ndelivery. It preserves live workers, Tasks, Dispatches, processes, and files.\nNever take over while the original coordinator is actively coordinating.\n\nDo not launch a replacement editor merely because Orca updated or authority is\nunclear. Keep the original worker as the only editor until a stable handoff\npoint, then use a fresh current Dispatch in a conflict-free placement.\n\n\n\n# Low-level topology\n\nLoad this reference only when `worker-start` cannot express required custom argv\nor terminal topology. It is not the normal supervised loop and is never a full\nhandoff recipe.\n\n```text\nORCA terminal create --worktree active --title --command \"\" --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\nORCA orchestration dispatch --task --to --inject --json\n```\n\nWait for readiness only when startup could lose injected input. Prefer\nagent-first `worker-start` whenever its argv and topology are sufficient.\n\n`dispatch --inject` creates authoritative Task/Dispatch context but deliberately\nkeeps an operator-created process unsupervised: it creates no supervised worker\nresource row. `worker-show`, `worker-read`, and `worker-list` report the lane as\n`unsupervised`; `worker-stop` and `worker-abandon` do not close that process, and\nsettled retain/release take no process action.\n\nUse `worker-start --terminal ` when lifecycle ownership of an existing\nagent terminal is required. Never imply that low-level dispatch retroactively\nowns a process, never use it to route around the nested-depth limit, and never\nuse it for an ownership handoff.\n\n\n\n# Messaging and gates\n\nLoad this reference for inbox replay, attempt-specific guidance, group\naddresses, blocking questions, or coordinator-managed DAG decisions.\n\nA successful `send` proves durable enqueue. Wake and nudge are best-effort\nattention only: neither proves the recipient read the message, began a turn, or\naccepted steering.\n\n## Coordinator delivery loop\n\n```text\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nA consuming coordinator `check` returns the bound Run's oldest FIFO Delivery,\nup to 50 messages, and replays that exact batch until acknowledged. Process\nevery row and required terminal ownership decision before `--ack`. Type filters\ndecide when a waiter wakes; they do not authorize skipping older actionable\nmail. `--peek` and `--all` are read-only inspection, not progress through the\ncoordinator inbox.\n\nAn empty wait or timeout is a checkpoint. Continue rolling waits until every\nexpected Dispatch settles. Heartbeat or visible activity means alive, not done.\n\n## Addresses\n\nUse a stable Dispatch address for attempt-specific coordinator guidance:\n\n```text\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\nDo not substitute a remote terminal handle. Omit `--from` for ordinary\ncoordinator calls; a dispatched worker instead copies the exact `--from` and\ncapability arguments in its preamble.\n\nGroup addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`,\n`@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:`. Use them only for\nintentional fan-out status or questions. `worker_done`, heartbeat, and other\nDispatch lifecycle messages never target groups.\n\n## Questions and gates\n\nA worker uses `ask`; its timeout leaves one durable question pending, which the\nworker resumes by message ID. The coordinator answers that message with `reply`.\n\nUse a gate only for a coordinator-owned Task-DAG decision:\n\n```text\nORCA orchestration gate-create --task --question \"\" --options --json\nORCA orchestration gate-resolve --id --resolution \"\" --json\nORCA orchestration gate-list --task --json\n```\n\nPass `json_array` using the quoting rules of the active shell; do not copy POSIX\nsingle-quote syntax into PowerShell or `cmd.exe`.\n\nDo not create a gate merely to answer a worker's `ask`.\n\n\n\n# Placement and remote execution\n\nLoad this reference before creating a new worktree or placing work through SSH,\nWSL, or another connected Orca server.\n\n## Placement choices\n\nA fresh worker means a fresh agent terminal, not a new Git worktree. Use the\ncurrent or an exact existing workspace by default. Create a worktree only when\nthe user requested one or a concrete checkout or filesystem conflict makes\nsharing unsafe.\n\n```text\n# Current workspace; setup is not rerun.\nORCA orchestration worker-start --task --worktree current --agent codex --json\n\n# Stacked child worktree.\nORCA orchestration worker-start --task --worktree new-child --name --agent codex --setup run --json\n\n# Independent top-level worktree.\nORCA orchestration worker-start --task --worktree new-top-level --name --agent codex --setup run --json\n```\n\nCurrent and exact existing workspaces create a fresh terminal unless\n`--terminal` is explicit. Folder workspaces are first-class; do not invoke Git\nor require worktree lineage when the selected workspace is a folder.\n\nNew worktrees use agent-first creation and run setup by default. Preserve the\nrepository's startup policy: `start-immediately` can report setup as `running`,\nwhile `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base,\nfilesystem isolation, coordination parentage, UI grouping, and execution host\nare separate decisions.\n\n## Connected servers\n\nThe Run and Tasks remain authoritative on the current server. `--on` selects\nonly the worker's execution server and appears only on `worker-start`:\n\n```text\nORCA orchestration worker-start --task --on --worktree new-top-level --repo --name --agent codex --setup run --json\n```\n\nRemote `current` and `new-child` are invalid because they are ambiguous across\nservers. Use an exact discovered remote workspace, or `new-top-level` with an\nexact remote repository selector. After start, route every follow-up, read,\nstop, and cleanup by Dispatch ID; never repeat `--on` or substitute a remote\nterminal handle.\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\n## Execution-host and mixed-version floor\n\nThe execution host owns process, filesystem, transcript, stop, and cleanup\nfacts. Render only `live`, `unverifiable`, or `exited`. Connection loss, relay\nabsence, missing client inventory, or timeout yields `unverifiable`, never\nsynthetic exit and never a client-local substitute action.\n\nClients and servers update independently. Optional response fields may be\nabsent. Forward model/effort, transcript reads, cleanup, or another new remote\noperation only when the peer advertises the relevant capability; unknown stream\nopcodes can be silently dropped. A narrow unsupported response may degrade to a\ndocumented older path, but must not broaden the target or cross the execution\nboundary. Changing host-published content reaches old clients even without a\nwire-shape change, so preserve established semantics or negotiate the behavior.\n\nFor WSL, use the exact executable and arguments returned by Orca so the distro\nand packaged launcher remain bound. Do not translate a printed `orca-ide`\nrecovery command into a PATH-resolved local command.\n\n\n\n# Recovery and cleanup\n\nLoad this reference only after a failed/stopped/unknown attempt, explicit retry\ndecision, stop/abandon request, retention request, or uncertain release.\n\n| Proven state | Safe action |\n| ---------------------- | ------------------------------------------------------------------ |\n| `ready` or active | Keep waiting; optionally read bounded output |\n| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |\n| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |\n| Accepted `worker_done` | Reuse, retain, or release |\n| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |\n\n## Inspect before acting\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\n```\n\n`worker-read --source auto` uses a proven provider transcript when available and\notherwise returns bounded terminal output with a typed `fallbackReason`.\nContinue with its top-level cursor, which is pinned to that source. If Orca\nreports `source_changed`, restart without the old cursor. A bounded initial\ntranscript tail can return an EOF cursor that follows only newly appended records;\nread `contentComplete`, `clipping`, and `warnings` before assuming omitted older\nrecords are pageable. Never guess a provider session ID, transcript path, or\nremote terminal handle.\n\n## Retry, stop, and abandon\n\nRetry only a positively proven failed or stopped attempt. Placement is never\nsilently inherited:\n\n```text\nORCA orchestration worker-start --task --retry-of --worktree --agent --json\n```\n\nAfter three consecutive failures for one Task, its dispatch context\ncircuit-breaks and the Task is failed. Do not route around that boundary with a\nnew Run or an unrelated Dispatch.\n\nFor `outcome_unknown`, inspect first, then make an explicit choice:\n\n```text\nORCA orchestration worker-stop --dispatch --json\nORCA orchestration worker-abandon --dispatch --json\n```\n\n`worker-stop` closes only the exact proven supervised agent terminal. It never\ndeletes the worktree, setup terminal, configured tabs, or unrelated processes.\n`worker-abandon` fences orchestration while accepting that resources may remain\nlive; it performs no remote, process, or filesystem action.\n\n## Retain and release\n\n```text\nORCA orchestration worker-retain --dispatch --json\nORCA orchestration worker-release --dispatch --json\n```\n\nRetain only when the user explicitly wants the settled terminal kept live.\nRelease works after succeeded and failed reports, archives readable output, and\ncloses only the exact terminal owned by that settled Dispatch. Replays may call\nrelease again safely. Reused, pre-existing, setup, coordinator, active,\nuser-taken-over, and unproven terminals are retained.\n\nNever release because of timeout, TUI idle, heartbeat, status, question,\nescalation, or stale/rejected completion. If the receipt says `release_pending`\nor `release_unknown`, follow its exact recovery action. Never substitute\n`terminal close`.\n\n`orchestration reset` is destructive recovery. Do not run it during active\ncoordination unless the user explicitly abandons that state.\n\n\n\n# Worker contract\n\nThe injected preamble is authoritative. Copy its command rather than\nreconstructing flags. In particular, preserve the exact executable, worker\nhandle, Dispatch capability, Task ID, and Dispatch ID.\n\n## Heartbeat\n\nSend heartbeats only at the cadence required by the live preamble. Skip them\nwhile blocked inside `ask` or `check --wait`; those calls are liveness signals.\n\n```text\nORCA orchestration send --from --dispatch-capability --type heartbeat --subject \"alive\" --task-id --dispatch-id --phase \"\"\n```\n\nUse typed lifecycle flags, not a hand-written JSON payload. A heartbeat proves\nliveness, never completion.\n\n## Ask and resume\n\nUse Orca `ask` whenever the coordinator must answer. Never open a local\nAskUserQuestion-style TUI: the coordinator cannot see or answer it.\n\n```text\nORCA orchestration ask --from --dispatch-capability --question \"\" --options \",\" --timeout-ms 600000\n\nORCA orchestration ask --from --dispatch-capability --resume --timeout-ms 600000\n```\n\nA timeout or disconnect leaves the original question pending. Resume its\nmessage ID; do not create a duplicate question.\n\n## Escalation\n\nEscalate only before completion and only when the coordinator must intervene:\n\n```text\nORCA orchestration send --from --dispatch-capability --type escalation --subject \"Blocked: \" --body \"
\" --task-id --dispatch-id \n```\n\n## Completion\n\nSend exactly one terminal report. `--body` is three sentences: what changed,\nwhat was found, and what remains. Use `--outcome failed` when the requested work\nis not complete; never hide failure in prose or silently exit.\n\nAppend `--files-modified` or `--report-path` only when applicable, using actual\npaths. Do not send documentation placeholders as metadata.\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nAfter `worker_done`, end the dispatched turn and idle. Do not poll, close your\nown terminal, or begin unrelated work. A later direct user instruction is new\nuser-owned work and must not reuse settled lifecycle IDs; a supervised follow-up\narrives with a fresh preamble and Task block.\n" +const ORCHESTRATION_FULL_MARKDOWN = "---\nname: orchestration\ndescription: >-\n Coordinate supervised Orca workers with durable Runs, Tasks, Dispatches,\n messages, questions, gates, and completion tracking. Use when the user asks\n to supervise, monitor, wait for results, coordinate a DAG, or manage blocking\n agent-to-agent questions. For full ownership handoffs or ordinary terminal,\n worktree, and built-in-browser control, use `orca-cli`. Use Computer Use for\n external browser windows, webviews, Orca app UI, or desktop UI outside Orca's\n embedded browser only when the task requires OS/window-level control such as\n focus, menus, dialogs, coordinates, or screenshots. Use `orca-cli` for Orca's\n embedded pages and a page-automation tool such as Playwright or CDP for\n external pages.\n---\n\n# Orca orchestration\n\nOrchestration is Orca's structured coordination layer. It records who owns work,\nwhich attempt is authoritative, and when supervised work has settled.\n\n## Outcome\n\n**Result:** every in-scope Task has one explicit outcome and every settled worker\nterminal has a next owner or cleanup decision. **Done:** all expected Dispatches\nhave settled, every delivered message was processed before acknowledgment, and\neach settled worker was reused, explicitly retained, or released.\n\n**Safe failure:** preserve work and authority and report the state as unknown or\n`unverifiable`. A timeout, quiet terminal, missing client, or lost remote\nconnection is never proof of failure or exit.\n\n## Classify the role\n\n| Current context | Role | Route |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------ |\n| The user explicitly asks to supervise, monitor, wait for results, track completion, coordinate a DAG, use a decision gate, or manage ask/reply | Coordinator | Use the supervised loop below |\n| The current prompt contains a live injected preamble with Task and Dispatch IDs | Dispatched worker | Follow the preamble and the worker obligations below |\n| The user asks to hand off ownership or start another agent/worktree without supervision | Handoff owner | Use `orca-cli`; create no Run, Task, or Dispatch and do not monitor completion |\n| A message carries a legacy authority label | Compatibility operator | Load the legacy contract reference before any lifecycle mutation |\n| No live preamble and no explicit supervision | Ordinary terminal agent | Do not emit lifecycle messages; use `orca-cli` for terminal/worktree work |\n\nModel or effort selection does not make a handoff supervised. Never substitute a\nnon-Orca subagent tool when Orca orchestration provenance was requested. Before\nclaiming a worker was orchestrated, verify its Task and Dispatch exist.\n\n## Authority and safety floor\n\n- A Run is a durable namespace and coordinator inbox; it does not schedule or\n place workers. A Task is work. A Dispatch is one authoritative Task attempt.\n- Lifecycle authority comes from the active Dispatch, not a terminal title,\n copied ID, old database row, provider transcript, or visible pane.\n- Workers use the exact executable, handle, capability, Task ID, and Dispatch ID\n in the live preamble. Never reconstruct, translate, or broaden those arguments.\n- After remote start, address the worker by Dispatch ID. The execution host owns\n process, filesystem, transcript, stop, and cleanup facts. Preserve the verdicts\n `live` / `unverifiable` / `exited`; contact loss is not process death.\n- Liveness is layered: `worker-list`'s `projection.liveness` is the fleet verdict\n for the agent; `worker-show`'s `observation.status` is PTY liveness only. A live\n terminal can still hold a dead or stuck agent.\n- Folder workspaces are valid. Do not require Git or assume every workspace is a\n worktree.\n- Worktree selectors need the full `::` value Orca returned,\n passed as `id:`; a bare repo id is not a worktree id.\n- Clients and remote servers update independently. Treat unknown optional fields\n as absent. A new stream operation requires advertised capability because old\n decoders may silently drop unknown opcodes. Never fall back to local execution\n when remote authority or capability is unproven.\n- Use the executable selected by the discovery stub for the entire run. In the\n examples below, replace `ORCA` with it; do not create a shell variable or run\n `ORCA` literally. If it fails, report that exact error instead of switching.\n- Legacy takeover binds the authenticated invoking terminal; `--from` cannot\n nominate another coordinator. Never take over while that coordinator is active.\n- A successful `orchestration send` proves durable enqueue; its wake or nudge is\n best-effort attention only and does not prove the recipient read or accepted it.\n\n## Worker obligations\n\nThe injected preamble is authoritative. A dispatched worker must:\n\n1. Do only the current Task and use the preamble's `ask` command for a blocking\n coordinator question. Never open a local question TUI the coordinator cannot\n answer. Resume the same message ID after an ask timeout.\n2. Send heartbeats only at the cadence in the preamble. A heartbeat proves\n liveness, not completion.\n3. Read coordinator follow-ups at each natural checkpoint — before starting a\n new file, after a test run — and once more immediately before `worker_done`:\n `ORCA orchestration check --terminal --json`.\n4. Send `worker_done` exactly once, from the dispatched terminal, with a\n three-sentence executive summary, both lifecycle IDs, and explicit\n `--outcome succeeded` or `--outcome failed`. Never encode failure only in prose.\n5. Append `--files-modified` and `--report-path` only with real values when\n applicable. After `worker_done`, end the dispatched turn and idle; do not poll\n or start new work.\n\nThe generic shape below is only a reminder. Copy the live preamble's command,\nincluding its executable, `--from`, and `--dispatch-capability` values:\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nA direct user instruction after completion starts new user-owned work and takes\nprecedence over the idle rule. Do not reuse the settled lifecycle IDs. A\ncoordinator-supervised follow-up arrives with a fresh preamble and Task block.\n\n## Canonical supervised loop\n\nConfirm the runtime, bind one Run, and start the full independent wave before\nwaiting. `worker-start --spec` creates the Task and its attempt in one call:\n\n```text\nORCA status --json\nORCA orchestration run-create --objective \"\" --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent codex --json\nORCA orchestration worker-start --spec \"\" --worktree current --agent claude --json\nORCA orchestration check --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nUse `task-create` plus `worker-start --task ` for planned fan-out with\ndependencies or a retry of a known Task. Use dependencies only for real ordering\nand prefer parallel waves over chains deeper than three or four steps; nested\nworkers obey the depth limit, and a new Run does not reset the caller's depth.\n\nA consuming `check` names its caller with `--terminal `, never `--from`;\nomit it inside the coordinator's own Orca terminal. It returns the bound Run's\noldest FIFO Delivery and replays that batch until acknowledged. Process every\nmessage: reply to questions, validate each `worker_done` against the expected\nactive Dispatch, and decide each settled terminal's next owner before the ack:\n\n```text\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration worker-release --dispatch --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\nKeep waiting until every expected Dispatch settles. A timeout or empty result is\na checkpoint, not a failure. Do not stop, retry, release, or launch a duplicate\neditor from timeout, idle state, heartbeat, relay loss, or missing client alone.\n\nAfter three consecutive empty waits, stop waiting blindly and enumerate with\n`ORCA orchestration worker-list --json`. Act on each row's `attention`,\n`requiresAction`, and literal `nextAction` argv. A worker that is not `live`,\nwhose `worker-show` reports `agentWait` null, and whose `worker-read` shows no\nnew progress is stalled, not working: load `references/recovery-and-cleanup.md`\nand choose `worker-stop` or `worker-abandon` explicitly.\n\n`worker-start` is the normal path, composing placement, terminal readiness,\nprompt injection, and supervised resource ownership. `dispatch --inject` leaves\nan operator-created process unsupervised and is only for an expressiveness gap.\n\n## Task-spec contract\n\nEvery Task spec must be self-contained and name:\n\n- **Target:** the files, component, or environment in scope.\n- **Change:** the concrete result to produce.\n- **Constraints:** invariants, compatibility rules, and do-not-touch boundaries.\n- **Ownership:** what this worker may edit and any coordination boundary.\n- **Observable acceptance:** the test, output, or evidence that proves completion.\n\n## Completion accounting\n\nAfter an accepted success or failure report, immediately do exactly one:\n\n1. Reuse the same proven agent terminal for an immediate follow-up Dispatch.\n2. Record user-requested retention with `worker-retain`.\n3. Run `worker-release`.\n\nRelease is post-settlement cleanup, not cancellation. Never release because of\nidle state, timeout, heartbeat, status, question, escalation, or a rejected or\nstale completion. If release is uncertain, follow its exact recovery receipt and\nnever substitute `terminal close`. Released output stays readable via\n`worker-read`.\n\nA valid `worker_done` settles the Task and Dispatch automatically; do not follow\nit with `task-update --status completed`. Enumerate the terminals still owing a\ndecision with `worker-list --terminal-state reclaimable --json`, and do not end\nthe coordinator turn until it returns none.\n\n## Conditional references\n\nThis compact guide is sufficient for the normal local loop. At an action gate\nbelow, run `ORCA skills get orchestration --full` once and read only the named\nbundled reference: it returns this exact kernel and every reference from the same\nCLI build. If an older CLI rejects `--full`, keep this kernel's safety floor, use\nthat command's `--help`, and never guess newer flags.\n\n| Action gate | Bundled reference |\n| ----------------------------------------------------------------------------------- | ----------------------------------------- |\n| Expanded DAG waves, launch model/effort, same-terminal reuse, or review ownership | `references/coordinator-loop.md` |\n| Worker ask/resume, heartbeat, escalation, or completion command details | `references/worker-contract.md` |\n| New worktree, exact workspace, SSH, WSL, or connected-server placement | `references/placement-and-remote.md` |\n| Inbox replay, follow-up messages, group addresses, or decision gates | `references/messaging-and-gates.md` |\n| Failed/stopped/unknown attempts, retry, stop, abandon, retain, or uncertain release | `references/recovery-and-cleanup.md` |\n| Custom argv or terminal topology that `worker-start` cannot express | `references/low-level-topology.md` |\n| Any legacy label, adopted Run, compatibility receipt, or takeover | `references/legacy-contract-migration.md` |\n\nRetired scheduler commands are not aliases for Run creation. Recovery commands\nmust provide their exact next action; follow it with the same selected executable.\n\n---\n\n# Bundled references\n\nThese references belong to the version-matched guide above. Read only the documents named by its action gates.\n\n\n\n# Coordinator loop\n\nLoad this reference for expanded DAG waves, per-invocation launch preferences,\nsame-terminal reuse, or review ownership. The compact guide remains the source\nof truth for the loop order and completion boundary.\n\n## Ready waves\n\nCreate independent Tasks before the first wait. Encode only real dependencies,\nthen use the ready view as external memory:\n\n```text\nORCA orchestration task-create --spec \"\" --deps --json\nORCA orchestration task-list --ready --brief --json\n```\n\n`--brief` collapses whitespace and caps echoed specs at 160 characters;\n`spec_truncated` identifies shortened rows. Omit it when full specs are needed or\nwhen an older CLI rejects the flag. A nested worker must respect\n`nested_worker_depth_exceeded`; creating another Run does not reset depth.\n\n## Launch preferences\n\nFor a fresh Claude, Codex, or Cursor terminal, `--model` accepts an opaque\nprovider model ID. Add `--effort` only when that model supports it:\n\n```text\nORCA orchestration worker-start --task --worktree current --agent claude --model opus --effort high --json\n```\n\n`--effort` requires `--model`; neither option combines with `--terminal`. A\nconnected worker server must advertise launch-preference support before Orca\nforwards either field. Compare `launch.requested` with `launch.effective`; never\nclaim a model or effort from requested arguments alone.\n\n## Reuse after settlement\n\nChoose the terminal's next owner before acknowledging the Delivery. When the\nsame exact agent has immediate follow-up work, recover the proven handle and\ntransfer cleanup ownership to the new Dispatch:\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-start --task --terminal --json\n```\n\nOtherwise explicitly retain or release the settled worker. Do not leave it live\nonly to inspect output; archived output remains available through `worker-read`.\n\n## Review ownership\n\nA review-only `worker_done` authorizes synthesis of findings, not coordinator\nfile edits. Dispatch or hand off fixes unless the user explicitly assigned them\nto the coordinator. If the user's plan names a next owner, post-review fixes and\nPR preparation remain with that owner; the coordinator routes and synthesizes.\n\n\n\n# Legacy contract migration\n\nLoad this reference only for an authority label, adopted Run, compatibility or\nrecovery receipt, or explicit legacy takeover. A newly created attempt always\nuses the current grammar.\n\n## Authority labels\n\n- `[LEGACY COMPATIBILITY]` is live and attested. Run only the exact supported\n command printed with the message, using the same selected executable and\n arguments supplied by the original prompt.\n- `[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]` is one bounded,\n at-least-once cutover replay. Process it idempotently and acknowledge only\n through the exact displayed guidance.\n- `[LEGACY READ-ONLY]` is inspection-only. It has no reply, acknowledgment, or\n lifecycle mutation.\n- An unlabeled current message uses the current guide and grammar.\n\nAn explicitly selected current Run, attested current binding, current Dispatch,\nor federated attachment takes precedence over legacy fallback. A retained\nadoption record alone does not grant mutation authority. If liveness, principal\nownership, capability, or the exact legacy contract is unproven, degrade to\nread-only inspection and never fall back to local execution.\n\nAdoption preserves the live agent process, PTY/session, terminal handle,\ntab/pane, worktree or folder workspace, Task, and Dispatch. It never restarts or\nreplaces the worker and never revives the retired scheduler. Loss of lifecycle\nauthority does not invalidate the existing process, assignment, or filesystem\nwork. Exact recovery may restore the same PTY once in its original inactive\nbackground tab; it must not spawn, write, signal, stop, switch, focus, split, or\ninject a terminal.\n\n## Compatibility recovery\n\nWhen a compatibility response returns structured next-step arguments, execute\nthose exact arguments with the same selected CLI executable. Do not translate\nfrom memory, broaden the recipient, or retry as a current mutation unless the\nreceipt explicitly authorizes it.\n\nA pending ask, reply, final Dispatch settlement, and consuming check have\ndurable recovery identities. Heartbeat and escalation remain at-least-once\nacross a manual contract-boundary retry. If an ask may already have been\nanswered, run the exact non-consuming recovery check printed by Orca before\ncreating any new question. Never guess among identical question threads.\n\nOn packaged Windows, a legacy ask uses a two-step commit/resume protocol. The\ninitial command commits the question, prints its exact\n`ask --resume ` command, and exits with launcher status `75`. Run\nthat exact resume after the launcher or update boundary. For an attested WSL\nlaunch, preserve the printed `orca-ide` executable and distro route. Older WSL\nworkers without launch proof remain lifecycle read-only even while their\nterminal and filesystem work continue.\n\n## Read-only inspection and takeover\n\nRead-only inspection does not consume mail:\n\n```text\nORCA orchestration run-list --json\nORCA orchestration run-show --id run_legacy_local --json\nORCA orchestration run-show --id --json\nORCA orchestration task-list --run --json\nORCA orchestration inbox --full --json\nORCA orchestration check --terminal --peek --format --json\nORCA terminal read --terminal --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\n```\n\n`run_legacy_local` is an empty audit tombstone after adoption. Find the ordinary\nRun whose objective is `Recovered orchestration work from a contract update`.\n\nOnly when the original coordinator is unavailable or cannot prove retained\nauthority may a new live coordinator take over from its own terminal:\n\n```text\nORCA orchestration run-use --id --takeover-legacy --json\nORCA orchestration check --run --json\n```\n\nTakeover fences only the old coordinator and moves pending mail into current Run\ndelivery. It preserves live workers, Tasks, Dispatches, processes, and files.\nNever take over while the original coordinator is actively coordinating.\n\nDo not launch a replacement editor merely because Orca updated or authority is\nunclear. Keep the original worker as the only editor until a stable handoff\npoint, then use a fresh current Dispatch in a conflict-free placement.\n\n\n\n# Low-level topology\n\nLoad this reference only when `worker-start` cannot express required custom argv\nor terminal topology. It is not the normal supervised loop and is never a full\nhandoff recipe.\n\n```text\nORCA terminal create --worktree active --title --command \"\" --json\nORCA terminal wait --terminal --for tui-idle --timeout-ms 60000 --json\nORCA orchestration dispatch --task --to --inject --json\n```\n\nWait for readiness only when startup could lose injected input. Prefer\nagent-first `worker-start` whenever its argv and topology are sufficient.\n\n`dispatch --inject` creates authoritative Task/Dispatch context but deliberately\nkeeps an operator-created process unsupervised: it creates no supervised worker\nresource row. `worker-show`, `worker-read`, and `worker-list` report the lane as\n`unsupervised`; `worker-stop` and `worker-abandon` do not close that process, and\nsettled retain/release take no process action.\n\nUse `worker-start --terminal ` when lifecycle ownership of an existing\nagent terminal is required. Never imply that low-level dispatch retroactively\nowns a process, never use it to route around the nested-depth limit, and never\nuse it for an ownership handoff.\n\n\n\n# Messaging and gates\n\nLoad this reference for inbox replay, attempt-specific guidance, group\naddresses, blocking questions, or coordinator-managed DAG decisions.\n\nA successful `send` proves durable enqueue. Wake and nudge are best-effort\nattention only: neither proves the recipient read the message, began a turn, or\naccepted steering.\n\n## Coordinator delivery loop\n\n```text\nORCA orchestration check --terminal --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\nORCA orchestration reply --id --body \"\" --json\nORCA orchestration check --ack --wait --types \"worker_done,escalation,question\" --timeout-ms 900000 --json\n```\n\n`check` names its caller with `--terminal ` and is the only verb that\nrejects `--from`. Omit `--terminal` inside an Orca terminal, where Orca resolves\nthe caller; pass it explicitly from anywhere else, including a dispatched\nworker reading coordinator follow-ups.\n\nA consuming coordinator `check` returns the bound Run's oldest FIFO Delivery,\nup to 50 messages, and replays that exact batch until acknowledged. Process\nevery row and required terminal ownership decision before `--ack`. Type filters\ndecide when a waiter wakes; they do not authorize skipping older actionable\nmail. `--peek` and `--all` are read-only inspection, not progress through the\ncoordinator inbox.\n\nAn empty wait or timeout is a checkpoint. Continue rolling waits until every\nexpected Dispatch settles. Heartbeat or visible activity means alive, not done.\n\n## Addresses\n\nUse a stable Dispatch address for attempt-specific coordinator guidance:\n\n```text\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\nDo not substitute a remote terminal handle. Omit `--from` for ordinary\ncoordinator calls; a dispatched worker instead copies the exact `--from` and\ncapability arguments in its preamble. `check` is the exception: it identifies\nits caller with `--terminal`, never `--from`.\n\nGroup addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`,\n`@gemini`, `@droid`, `@grok`, `@cursor`, and `@worktree:`. Use them only for\nintentional fan-out status or questions. `worker_done`, heartbeat, and other\nDispatch lifecycle messages never target groups.\n\n## Questions and gates\n\nA worker uses `ask`; its timeout leaves one durable question pending, which the\nworker resumes by message ID. The coordinator answers that message with `reply`.\n\nUse a gate only for a coordinator-owned Task-DAG decision:\n\n```text\nORCA orchestration gate-create --task --question \"\" --options --json\nORCA orchestration gate-resolve --id --resolution \"\" --json\nORCA orchestration gate-list --task --json\n```\n\nPass `json_array` using the quoting rules of the active shell; do not copy POSIX\nsingle-quote syntax into PowerShell or `cmd.exe`.\n\nDo not create a gate merely to answer a worker's `ask`.\n\n\n\n# Placement and remote execution\n\nLoad this reference before creating a new worktree or placing work through SSH,\nWSL, or another connected Orca server.\n\n## Placement choices\n\nA fresh worker means a fresh agent terminal, not a new Git worktree. Use the\ncurrent or an exact existing workspace by default. Create a worktree only when\nthe user requested one or a concrete checkout or filesystem conflict makes\nsharing unsafe.\n\n```text\n# Current workspace; setup is not rerun.\nORCA orchestration worker-start --task --worktree current --agent codex --json\n\n# Stacked child worktree.\nORCA orchestration worker-start --task --worktree new-child --name --agent codex --setup run --json\n\n# Independent top-level worktree.\nORCA orchestration worker-start --task --worktree new-top-level --name --agent codex --setup run --json\n```\n\nCurrent and exact existing workspaces create a fresh terminal unless\n`--terminal` is explicit. Folder workspaces are first-class; do not invoke Git\nor require worktree lineage when the selected workspace is a folder.\n\nRegister a folder workspace through project setup. `repo add --path `\nrequires a valid Git repository and rejects a plain directory:\n\n```text\nORCA project setup-existing-folder --project --host --path --kind folder --json\n```\n\nThen place work on the returned workspace with an exact selector. `new-child`\nand `new-top-level` are worktree creation and do not apply to a folder.\n\nNew worktrees use agent-first creation and run setup by default. Preserve the\nrepository's startup policy: `start-immediately` can report setup as `running`,\nwhile `wait-for-setup` gates prompt delivery on success. Orca lineage, Git base,\nfilesystem isolation, coordination parentage, UI grouping, and execution host\nare separate decisions.\n\n## Connected servers\n\nThe Run and Tasks remain authoritative on the current server. `--on` selects\nonly the worker's execution server and appears only on `worker-start`:\n\n```text\nORCA orchestration worker-start --task --on --worktree new-top-level --repo --name --agent codex --setup run --json\n```\n\nRemote `current` and `new-child` are invalid because they are ambiguous across\nservers. Use an exact discovered remote workspace, or `new-top-level` with an\nexact remote repository selector. After start, route every follow-up, read,\nstop, and cleanup by Dispatch ID; never repeat `--on` or substitute a remote\nterminal handle.\n\n```text\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\nORCA orchestration send --to dispatch: --subject \"Follow-up\" --body \"\" --json\n```\n\n## Execution-host and mixed-version floor\n\nThe execution host owns process, filesystem, transcript, stop, and cleanup\nfacts. Render only `live`, `unverifiable`, or `exited`. Connection loss, relay\nabsence, missing client inventory, or timeout yields `unverifiable`, never\nsynthetic exit and never a client-local substitute action.\n\nClients and servers update independently. Optional response fields may be\nabsent. Forward model/effort, transcript reads, cleanup, or another new remote\noperation only when the peer advertises the relevant capability; unknown stream\nopcodes can be silently dropped. A narrow unsupported response may degrade to a\ndocumented older path, but must not broaden the target or cross the execution\nboundary. Changing host-published content reaches old clients even without a\nwire-shape change, so preserve established semantics or negotiate the behavior.\n\nFor WSL, use the exact executable and arguments returned by Orca so the distro\nand packaged launcher remain bound. Do not translate a printed `orca-ide`\nrecovery command into a PATH-resolved local command.\n\n\n\n# Recovery and cleanup\n\nLoad this reference only after a failed/stopped/unknown attempt, explicit retry\ndecision, stop/abandon request, retention request, or uncertain release.\n\n| Proven state | Safe action |\n| ----------------------- | ------------------------------------------------------------------ |\n| `ready` or active | Keep waiting; optionally read bounded output |\n| `failed` or `stopped` | Start a replacement with `--retry-of`; repeat placement explicitly |\n| `outcome_unknown` | Inspect, then choose `worker-stop` or explicit `worker-abandon` |\n| Accepted `worker_done` | Reuse, retain, or release |\n| Remote contact lost | Preserve `unverifiable`; do not stop or retry from absence alone |\n| Live PTY, stalled agent | Enumerate with `worker-list`; follow its `nextAction` |\n\n## Inspect before acting\n\n```text\nORCA orchestration worker-list --json\nORCA orchestration worker-show --dispatch --json\nORCA orchestration worker-read --dispatch --limit 50 --json\n```\n\n`worker-list` is the enumerating command and the authority on agent liveness:\neach row carries `projection.liveness`, `attention` categories, `requiresAction`,\nand a literal `nextAction` argv to run. `worker-show`'s `observation.status` is\nPTY liveness only, so a `live` terminal whose agent died at a trust prompt still\nreads `live` there. When the two disagree, the fleet verdict decides. A worker\nthat is not `live`, reports `agentWait` null, and shows no new `worker-read`\nprogress is stalled: stop waiting and choose `worker-stop` or `worker-abandon`.\n\n`worker-read --source auto` uses a proven provider transcript when available and\notherwise returns bounded terminal output with a typed `fallbackReason`.\nContinue with its top-level cursor, which is pinned to that source. If Orca\nreports `source_changed`, restart without the old cursor. A bounded initial\ntranscript tail can return an EOF cursor that follows only newly appended records;\nread `contentComplete`, `clipping`, and `warnings` before assuming omitted older\nrecords are pageable. Never guess a provider session ID, transcript path, or\nremote terminal handle.\n\n## Was the mutation applied?\n\nWhen a mutation's response was lost and named no Dispatch, do not replay blind.\nEvery orchestration mutation accepts `--retry-request `, which reuses one\noperation identity so Orca can replay, join, or recover it instead of starting a\nduplicate. Ask what happened first:\n\n```text\nORCA orchestration request-show --request --json\n```\n\n`completed` means the mutation already took effect; read its recorded receipt\ninstead of rerunning. `pending` means the original mutation is still running or\nOrca restarted before recording its outcome; replay the original command with\n`--retry-request `. `absent` means this runtime holds no receipt\nunder your caller identity — that is not proof nothing happened, so inspect the\naffected Task, Dispatch, and terminal before deciding whether to retry.\n\nWhen a worker's terminal accepted input but the submit is unconfirmed, use\n`terminal send --wait-submit `: it observes the accepted prompt for that\nlong and, on timeout, returns the input-accepted receipt without resending.\n\n## Retry, stop, and abandon\n\nRetry only a positively proven failed or stopped attempt. Placement is never\nsilently inherited:\n\n```text\nORCA orchestration worker-start --task --retry-of --worktree --agent --json\n```\n\nAfter three consecutive failures for one Task, its dispatch context\ncircuit-breaks and the Task is failed. Do not route around that boundary with a\nnew Run or an unrelated Dispatch.\n\nFor `outcome_unknown`, inspect first, then make an explicit choice:\n\n```text\nORCA orchestration worker-stop --dispatch --json\nORCA orchestration worker-abandon --dispatch --json\n```\n\n`worker-stop` closes only the exact proven supervised agent terminal. It never\ndeletes the worktree, setup terminal, configured tabs, or unrelated processes.\n`worker-abandon` fences orchestration while accepting that resources may remain\nlive; it performs no remote, process, or filesystem action.\n\n## Retain and release\n\n```text\nORCA orchestration worker-retain --dispatch --json\nORCA orchestration worker-release --dispatch --json\n```\n\nRetain only when the user explicitly wants the settled terminal kept live.\nRelease works after succeeded and failed reports, archives readable output, and\ncloses only the exact terminal owned by that settled Dispatch. Replays may call\nrelease again safely. Reused, pre-existing, setup, coordinator, active,\nuser-taken-over, and unproven terminals are retained.\n\nNever release because of timeout, TUI idle, heartbeat, status, question,\nescalation, or stale/rejected completion. If the receipt says `release_pending`\nor `release_unknown`, follow its exact recovery action. Never substitute\n`terminal close`.\n\n`orchestration reset` is destructive recovery. Do not run it during active\ncoordination unless the user explicitly abandons that state.\n\n\n\n# Worker contract\n\nThe injected preamble is authoritative. Copy its command rather than\nreconstructing flags. In particular, preserve the exact executable, worker\nhandle, Dispatch capability, Task ID, and Dispatch ID.\n\n## Heartbeat\n\nSend heartbeats only at the cadence required by the live preamble. Skip them\nwhile blocked inside `ask` or `check --wait`; those calls are liveness signals.\n\n```text\nORCA orchestration send --from --dispatch-capability --type heartbeat --subject \"alive\" --task-id --dispatch-id --phase \"\"\n```\n\nUse typed lifecycle flags, not a hand-written JSON payload. A heartbeat proves\nliveness, never completion.\n\n## Ask and resume\n\nUse Orca `ask` whenever the coordinator must answer. Never open a local\nAskUserQuestion-style TUI: the coordinator cannot see or answer it.\n\n```text\nORCA orchestration ask --from --dispatch-capability --question \"\" --options \",\" --timeout-ms 600000\n\nORCA orchestration ask --from --dispatch-capability --resume --timeout-ms 600000\n```\n\nA timeout or disconnect leaves the original question pending. Resume its\nmessage ID; do not create a duplicate question.\n\n## Reading coordinator follow-ups\n\nThe coordinator steers a running worker with `send --to dispatch:`. That\nenqueue is durable but does not interrupt you, so nothing arrives unless you\nlook:\n\n```text\nORCA orchestration check --terminal --json\n```\n\nRun it at each natural checkpoint — before starting a new file, after a test\nrun — and once more immediately before `worker_done`, so a redirect or a\ncancellation lands before the Task settles. `check` names its caller with\n`--terminal`, never `--from`. Stop checking after `worker_done`.\n\n## Escalation\n\nEscalate only before completion and only when the coordinator must intervene:\n\n```text\nORCA orchestration send --from --dispatch-capability --type escalation --subject \"Blocked: \" --body \"
\" --task-id --dispatch-id \n```\n\n## Completion\n\nSend exactly one terminal report. `--body` is three sentences: what changed,\nwhat was found, and what remains. Use `--outcome failed` when the requested work\nis not complete; never hide failure in prose or silently exit.\n\nAppend `--files-modified` or `--report-path` only when applicable, using actual\npaths. Do not send documentation placeholders as metadata.\n\n```text\nORCA orchestration send --from --dispatch-capability --type worker_done --subject \"\" --body \"\" --task-id --dispatch-id --outcome succeeded\n```\n\nAfter `worker_done`, end the dispatched turn and idle. Do not poll, close your\nown terminal, or begin unrelated work. A later direct user instruction is new\nuser-owned work and must not reuse settled lifecycle IDs; a supervised follow-up\narrives with a fresh preamble and Task block.\n" // oxfmt-ignore export const BUNDLED_SKILL_GUIDES = [ From dcc540102d5cf20f10e4af03d0d8a171c6e28c85 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 01:57:37 -0400 Subject: [PATCH 2/4] fix(orchestration): give a dispatched worker a concrete follow-up read cadence The dispatch address is durable but never interrupts a worker, so a preamble that only listed `check --terminal` produced workers that never read one coordinator follow-up. Name the checkpoints and the pre-worker_done read. --- .../__snapshots__/preamble.test.ts.snap | 7 +++++-- src/main/runtime/orchestration/preamble.test.ts | 13 ++++++++++++- src/main/runtime/orchestration/preamble.ts | 7 +++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/runtime/orchestration/__snapshots__/preamble.test.ts.snap b/src/main/runtime/orchestration/__snapshots__/preamble.test.ts.snap index 2b479e637dc..6557589260e 100644 --- a/src/main/runtime/orchestration/__snapshots__/preamble.test.ts.snap +++ b/src/main/runtime/orchestration/__snapshots__/preamble.test.ts.snap @@ -56,8 +56,11 @@ Slack, GitHub comments, or any other channel to reach a human during the run. # coordinator to do something before you can continue): orca orchestration send --from term_WORKER --type escalation --subject "Blocked: " --body "
" --task-id task_SNAP --dispatch-id ctx_SNAP - # Check for messages from the coordinator: - orca orchestration check --terminal term_WORKER + # Read coordinator follow-ups. Nothing interrupts you: a durable message only + # arrives when you look, so run this at each natural checkpoint — before you + # start a new file and after a test run — and once more immediately before + # you send worker_done, so a redirect lands before the task settles. + orca orchestration check --terminal term_WORKER --json === AFTER YOU SEND worker_done === diff --git a/src/main/runtime/orchestration/preamble.test.ts b/src/main/runtime/orchestration/preamble.test.ts index e5e363a88d6..2631cae2773 100644 --- a/src/main/runtime/orchestration/preamble.test.ts +++ b/src/main/runtime/orchestration/preamble.test.ts @@ -129,7 +129,18 @@ describe('buildDispatchPreamble', () => { expect(result).toMatch(/orchestration ask --from term_worker/) expect(result).toMatch(/orchestration send --from term_worker --type escalation/) expect(result).toContain('--task-id task_abc123 --dispatch-id ctx_def456') - expect(result).toContain('orchestration check --terminal term_worker') + expect(result).toContain('orchestration check --terminal term_worker --json') + }) + + it('gives the worker a concrete cadence for reading coordinator follow-ups', () => { + const result = buildDispatchPreamble(baseParams()) + const checkLine = result.indexOf('orchestration check --terminal term_worker --json') + const cadence = result.slice(0, checkLine) + + // Why: the transport is durable but never interrupts, so "you may check" produced + // workers that never read a single follow-up. + expect(cadence).toContain('before you\n # start a new file and after a test run') + expect(cadence).toContain('immediately before\n # you send worker_done') }) it('carries the minted Dispatch capability on lifecycle and question commands', () => { diff --git a/src/main/runtime/orchestration/preamble.ts b/src/main/runtime/orchestration/preamble.ts index ca8acdfa3ce..8ddc2277695 100644 --- a/src/main/runtime/orchestration/preamble.ts +++ b/src/main/runtime/orchestration/preamble.ts @@ -115,8 +115,11 @@ Slack, GitHub comments, or any other channel to reach a human during the run. # coordinator to do something before you can continue): ${cli} orchestration send --from ${params.workerHandle}${capabilityFlag} --type escalation --subject "Blocked: " --body "
" --task-id ${params.taskId} --dispatch-id ${params.dispatchId} - # Check for messages from the coordinator: - ${cli} orchestration check --terminal ${params.workerHandle} + # Read coordinator follow-ups. Nothing interrupts you: a durable message only + # arrives when you look, so run this at each natural checkpoint — before you + # start a new file and after a test run — and once more immediately before + # you send worker_done, so a redirect lands before the task settles. + ${cli} orchestration check --terminal ${params.workerHandle} --json ${postDoneInstructions}` From 8d142d2986a50b0034f075fee4b7e27bda9733ed Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 02:00:36 -0400 Subject: [PATCH 3/4] fix(cli): make selector_not_found name the offending worktree selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare repo id passed to --worktree returned a content-free `selector_not_found` with no value and no grammar, so a caller could not tell what was wrong. Shape it at the CLI boundary — the only layer that still knows what was typed — in the same selector/suggestions/nextSteps shape as an unknown-flag error, and point `--from` on `orchestration check` at `--terminal`, which edit distance cannot reach. --- src/cli/args.test.ts | 20 ++++++++++ src/cli/command-suggestion.ts | 12 +++++- src/cli/format-recovery.test.ts | 51 ++++++++++++++++++++++++- src/cli/format.ts | 36 +++++++++++++++++- src/cli/index.test.ts | 23 +++++++++++ src/cli/index.ts | 6 ++- src/cli/worktree-selector-recovery.ts | 55 +++++++++++++++++++++++++++ 7 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 src/cli/worktree-selector-recovery.ts diff --git a/src/cli/args.test.ts b/src/cli/args.test.ts index 1ac86d99e12..d94b8447de2 100644 --- a/src/cli/args.test.ts +++ b/src/cli/args.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import type { CommandSpec } from './args' +import { COMMAND_SPECS } from './specs' import { REPEATED_FLAG_SEPARATOR, findCommandSpec, @@ -325,6 +326,25 @@ describe('validateCommandAndFlags', () => { } }) + it('points --from at --terminal on the one verb that renamed the caller flag', () => { + const parsed = parseArgs(['orchestration', 'check', '--from', 'term_a']) + + try { + validateCommandAndFlags(COMMAND_SPECS, parsed) + throw new Error('expected validateCommandAndFlags to throw') + } catch (error) { + const data = (error as { data?: { suggestions: string[]; nextSteps: string[] } }).data + expect(data?.suggestions[0]).toBe('terminal') + expect(data?.nextSteps[0]).toContain('--terminal') + } + }) + + it('leaves --from alone where the command actually accepts it', () => { + const parsed = parseArgs(['orchestration', 'reply', '--from', 'term_a']) + + expect(() => validateCommandAndFlags(COMMAND_SPECS, parsed)).not.toThrow() + }) + it('attaches did-you-mean suggestions to unknown-command errors', () => { const suggestSpecs: CommandSpec[] = [ { diff --git a/src/cli/command-suggestion.ts b/src/cli/command-suggestion.ts index 7b80138e2f3..09113c853c8 100644 --- a/src/cli/command-suggestion.ts +++ b/src/cli/command-suggestion.ts @@ -105,10 +105,20 @@ export type FlagErrorData = { nextSteps: string[] } +// Why: edit distance cannot recover a rename. `orchestration check` is the one verb +// that identifies its caller with `--terminal` while every sibling uses `--from`, so +// the near-miss ranking answered `--json`/`--run` and left the caller stuck (#16904). +// A synonym only fires where the typed flag is rejected and its partner is accepted. +const FLAG_SYNONYMS: Readonly> = { from: 'terminal' } + function suggestFlags(flag: string, validFlags: string[]): string[] { - return rankByDistance( + const synonym = FLAG_SYNONYMS[flag] + const ranked = rankByDistance( validFlags.map((candidate) => ({ label: candidate, distance: levenshtein(flag, candidate) })) ) + return synonym && validFlags.includes(synonym) + ? [synonym, ...ranked.filter((name) => name !== synonym)].slice(0, MAX_SUGGESTIONS) + : ranked } // Why: include the accepted set so agents can recover without another help call. diff --git a/src/cli/format-recovery.test.ts b/src/cli/format-recovery.test.ts index fea52395cd8..4355b728a1f 100644 --- a/src/cli/format-recovery.test.ts +++ b/src/cli/format-recovery.test.ts @@ -1,8 +1,55 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' -import { formatCliError } from './format' +import { formatCliError, reportCliError } from './format' import { RuntimeClientError, RuntimeRpcFailureError } from './runtime-client' +function selectorNotFound(): RuntimeRpcFailureError { + return new RuntimeRpcFailureError({ + id: 'req_selector', + ok: false, + error: { code: 'selector_not_found', message: 'selector_not_found' }, + _meta: { runtimeId: 'runtime_local' } + }) +} + +describe('worktree selector recovery', () => { + it('names the offending value and the valid forms on a bare repo id', () => { + const output = formatCliError(selectorNotFound(), { + commandPath: ['orchestration', 'worker-start'], + worktreeSelector: 'id:github:stablyai/orca' + }) + + expect(output).toContain('No Orca workspace matched the worktree selector') + expect(output).toContain('id:github:stablyai/orca') + expect(output).toContain('Did you mean: id:github:stablyai/orca::') + expect(output).toContain('Valid selector forms:') + expect(output).toContain('a bare repository id is not a worktree id') + }) + + it('carries the same recovery into the --json failure envelope', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + + reportCliError(selectorNotFound(), true, { + commandPath: ['terminal', 'create'], + worktreeSelector: 'path:/nope' + }) + + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({ + error: { + code: 'selector_not_found', + data: { selector: 'path:/nope', validSelectorForms: expect.arrayContaining(['current']) } + } + }) + log.mockRestore() + }) + + it('stays silent when no worktree selector was passed', () => { + expect(formatCliError(selectorNotFound(), { commandPath: ['worktree', 'show'] })).toBe( + 'selector_not_found' + ) + }) +}) + describe('CLI error recovery', () => { it('prints did-you-mean next steps for an unknown-command error carrying data', () => { const error = new RuntimeClientError('invalid_argument', 'Unknown command: worktree remov', { diff --git a/src/cli/format.ts b/src/cli/format.ts index 0a297138364..968a0910d76 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -5,6 +5,7 @@ import { stripAutomationOwnerConflictCode } from '../shared/automation-owner-conflict' import { automationOwnerConflictRecovery } from './automation-owner-conflict-recovery' +import { worktreeSelectorRecovery } from './worktree-selector-recovery' import { prepareComputerCliJsonResult } from './computer-format' import type { RuntimeRpcFailure, RuntimeRpcSuccess } from './runtime-client' import { RuntimeClientError, RuntimeRpcFailureError } from './runtime/types' @@ -69,6 +70,21 @@ export { type CliErrorContext = { commandPath?: readonly string[] + /** The `--worktree` value this invocation sent; the runtime's error never echoes it. */ + worktreeSelector?: string +} + +function selectorRecovery(code: string | undefined, context: CliErrorContext) { + return code === 'selector_not_found' && context.worktreeSelector + ? worktreeSelectorRecovery(context.worktreeSelector) + : undefined +} + +function errorCode(error: unknown): string | undefined { + if (error instanceof RuntimeRpcFailureError) { + return error.response.error.code + } + return error instanceof RuntimeClientError ? error.code : undefined } export function printResult( @@ -85,6 +101,10 @@ export function printResult( export function formatCliError(error: unknown, context: CliErrorContext = {}): string { const message = error instanceof Error ? error.message : String(error) + const selector = selectorRecovery(errorCode(error), context) + if (selector) { + return formatMessageWithNextSteps(message, selector.nextSteps) + } if (error instanceof RuntimeClientError && error.code === 'runtime_unavailable') { if (hasOrchestrationRequestId(error.data)) { return message @@ -130,9 +150,19 @@ function hasOrchestrationRequestId(data: unknown): boolean { } export function reportCliError(error: unknown, json: boolean, context: CliErrorContext = {}): void { + const selector = selectorRecovery(errorCode(error), context) if (json) { if (error instanceof RuntimeRpcFailureError) { - console.log(JSON.stringify(withAutomationOwnerConflictRecovery(error.response), null, 2)) + const response = withAutomationOwnerConflictRecovery(error.response) + console.log( + JSON.stringify( + selector + ? { ...response, error: { ...response.error, data: response.error.data ?? selector } } + : response, + null, + 2 + ) + ) } else { const response: RuntimeRpcFailure = { id: 'local', @@ -201,6 +231,10 @@ function localCliErrorData(error: unknown, context: CliErrorContext): unknown { if (error instanceof RuntimeClientError && error.data !== undefined) { return error.data } + const selector = selectorRecovery(errorCode(error), context) + if (selector) { + return selector + } const conflict = automationOwnerConflictRecovery(matchAutomationOwnerConflict(error)) if (conflict) { return conflict diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 8504f647392..7308d39dac6 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -228,6 +228,29 @@ describe('unknown command surfaces a suggestion', () => { expect(stderr).toContain('--json') }) + it('names the offending --worktree value and the valid forms on selector_not_found', async () => { + const { RuntimeRpcFailureError } = await import('./runtime/types.js') + callMock.mockRejectedValue( + new RuntimeRpcFailureError({ + id: 'req_selector', + ok: false, + error: { code: 'selector_not_found', message: 'selector_not_found' }, + _meta: { runtimeId: 'runtime_local' } + }) + ) + + await main( + ['orchestration', 'worker-start', '--task', 't1', '--worktree', 'repo-1', '--agent', 'codex'], + '/tmp/repo' + ) + + expect(process.exitCode).toBe(1) + const stderr = errorSpy.mock.calls.map((call) => String(call[0])).join('\n') + expect(stderr).toContain('No Orca workspace matched the worktree selector "repo-1"') + expect(stderr).toContain('id:repo-1::') + expect(stderr).toContain('Valid selector forms:') + }) + it('reports a pre-command flag that belongs to another command', async () => { await main(['--workspace', 'worktree', 'list'], '/tmp/repo') diff --git a/src/cli/index.ts b/src/cli/index.ts index 9389113b195..01aa1d9e4be 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -176,7 +176,11 @@ export async function main( json }) } catch (error) { - reportCliError(error, json, { commandPath: parsed.commandPath }) + const worktreeSelector = parsed.flags.get('worktree') + reportCliError(error, json, { + commandPath: parsed.commandPath, + ...(typeof worktreeSelector === 'string' ? { worktreeSelector } : {}) + }) process.exitCode = 1 } } diff --git a/src/cli/worktree-selector-recovery.ts b/src/cli/worktree-selector-recovery.ts new file mode 100644 index 00000000000..ac597fc6c8c --- /dev/null +++ b/src/cli/worktree-selector-recovery.ts @@ -0,0 +1,55 @@ +// Why: the runtime answers an unresolvable `--worktree` with a bare +// `selector_not_found` — no offending value and no grammar — so a caller who passed +// a repo id where a worktree id belongs cannot tell what was wrong (#16904). The CLI +// is the only layer that still knows what the caller typed, so it shapes the recovery +// here, in the same validFlags/suggestions/nextSteps shape as an unknown-flag error. + +export const WORKTREE_SELECTOR_FORMS = [ + 'id:::', + 'path:', + 'name:', + 'branch:', + 'identity:', + 'issue:', + 'current', + 'active' +] as const + +export type WorktreeSelectorRecovery = { + selector: string + validSelectorForms: readonly string[] + suggestions: readonly string[] + nextSteps: readonly string[] +} + +const PREFIXES = ['id:', 'path:', 'name:', 'branch:', 'identity:', 'issue:'] + +function suggestForms(selector: string): string[] { + if (selector.startsWith('id:')) { + // A worktree id is `::`; the repo id alone names no checkout. + return selector.includes('::') + ? [] + : [`id:${selector.slice(3)}::`, 'path:'] + } + if (PREFIXES.some((prefix) => selector.startsWith(prefix))) { + return [] + } + return selector.startsWith('/') || /^[A-Za-z]:[\\/]/.test(selector) + ? [`path:${selector}`] + : [`id:${selector}::`, `name:${selector}`, `branch:${selector}`] +} + +export function worktreeSelectorRecovery(selector: string): WorktreeSelectorRecovery { + const suggestions = suggestForms(selector) + return { + selector, + validSelectorForms: WORKTREE_SELECTOR_FORMS, + suggestions, + nextSteps: [ + `No Orca workspace matched the worktree selector "${selector}".`, + ...(suggestions.length > 0 ? [`Did you mean: ${suggestions.join(', ')}`] : []), + `Valid selector forms: ${WORKTREE_SELECTOR_FORMS.join(', ')}.`, + 'List the exact values with `orca worktree list --json`; a bare repository id is not a worktree id.' + ] + } +} From b1c56e2cbe98fe581aa1e4247d4c8f43fdfb82fe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 02:11:38 -0400 Subject: [PATCH 4/4] fix(orchestration): stop worker receipts from contradicting themselves worker-show spread the raw worker row beside its parsed copies, so a reader got residual_resources (a JSON string) next to residualResources (an array), plus host_scope as JSON-inside-JSON and two authority hashes with no consumer. Parse once, emit camelCase once, and withhold the hashes. worker-show also published only PTY liveness, so an agent that died at a trust prompt read live there while worker-list called it unverifiable -- and worker-list's nextAction pointed back at worker-show. Both now publish the same fleet projection. worker-list's projection.resource restated fields the row already carried, and the unconfirmed-stop sentence doubled a terminator on an already-punctuated reason. --- .../worker-observation-handlers.ts | 31 ++++-- .../orchestration-federation-setup.test.ts | 2 +- ...ration-manual-dispatch-observation.test.ts | 15 ++- .../methods/orchestration-worker-control.ts | 21 ++-- .../orchestration-worker-list-method.ts | 10 +- .../orchestration-worker-observation.test.ts | 62 ++++++++++- .../orchestration-worker-observation.ts | 100 ++++++++++++++---- .../orchestration-workers-recovery.test.ts | 4 +- src/shared/pty-liveness-verdict.test.ts | 28 +++++ src/shared/pty-liveness-verdict.ts | 10 +- 10 files changed, 232 insertions(+), 51 deletions(-) create mode 100644 src/shared/pty-liveness-verdict.test.ts diff --git a/src/cli/handlers/orchestration/worker-observation-handlers.ts b/src/cli/handlers/orchestration/worker-observation-handlers.ts index 29841f7390f..018683fc285 100644 --- a/src/cli/handlers/orchestration/worker-observation-handlers.ts +++ b/src/cli/handlers/orchestration/worker-observation-handlers.ts @@ -15,22 +15,37 @@ import { formatWorkerRead, type LegacyWorkerReadResult } from './worker-output' export const ORCHESTRATION_WORKER_OBSERVATION_HANDLERS: Record = { 'orchestration worker-show': async ({ flags, client, json }) => { const result = await client.call<{ - dispatch: { id: string; task_id: string; status: string } - worker: { state: string; stage: string; agent_terminal_handle: string | null } + dispatch: { id: string; task_id: string; status: string } | null + worker: { state: string; stage: string; agentTerminalHandle: string | null } + projection?: { liveness: { verdict: string }; nextAction: { argv: string[] } } | null observation?: { agentWait?: { source: string; reason?: string } | null } }>('orchestration.workerShow', { dispatch: getRequiredStringFlag(flags, 'dispatch') }) printResult(result, json, (value) => { - const base = `${value.dispatch.id} task=${value.dispatch.task_id} [${value.worker.state}] stage=${value.worker.stage}` + const lines = [ + `${value.dispatch?.id ?? 'unknown'} task=${value.dispatch?.task_id ?? 'unknown'} [${value.worker.state}] stage=${value.worker.stage}` + ] + // Why: PTY status alone read `live` for an agent that died at a trust prompt, so the + // fleet verdict and its next action print beside it rather than in another command. + if (value.projection) { + lines.push( + `Agent liveness: ${value.projection.liveness.verdict}`, + `Next action: ${value.projection.nextAction.argv.join(' ') || 'none'}` + ) + } // Why: absent means unknown on older runtimes, distinct from an evaluated null wait. if (value.observation === undefined || !('agentWait' in value.observation)) { - return `${base}\nInteractive wait: unknown (not evaluated)` + lines.push('Interactive wait: unknown (not evaluated)') + } else if (value.observation.agentWait) { + const wait = value.observation.agentWait + lines.push( + `Waiting on a human: ${wait.reason ?? 'interactive prompt'} (via ${wait.source})` + ) + } else { + lines.push('Interactive wait: none') } - const wait = value.observation.agentWait - return wait - ? `${base}\nWaiting on a human: ${wait.reason ?? 'interactive prompt'} (via ${wait.source})` - : `${base}\nInteractive wait: none` + return lines.join('\n') }) }, diff --git a/src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts b/src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts index c1d91c18479..e275760c80b 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation-setup.test.ts @@ -187,7 +187,7 @@ describe('orchestration federated setup evidence', () => { worker: { state: 'ready', stage: 'input_accepted', - setup_state: 'failed', + setupState: 'failed', effects: expect.arrayContaining([ expect.objectContaining({ kind: 'setup', state: 'failed' }), expect.objectContaining({ kind: 'dispatch_input', state: 'accepted' }) diff --git a/src/main/runtime/rpc/methods/orchestration-manual-dispatch-observation.test.ts b/src/main/runtime/rpc/methods/orchestration-manual-dispatch-observation.test.ts index c99fcb1c328..409eaafa836 100644 --- a/src/main/runtime/rpc/methods/orchestration-manual-dispatch-observation.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-manual-dispatch-observation.test.ts @@ -156,6 +156,7 @@ describe('manual Dispatch observation', () => { workerState: string terminalState: string | null agentTerminalHandle: string | null + projection: { liveness: { verdict: string } } }[] } expect(workerList.workers).toEqual([ @@ -167,12 +168,18 @@ describe('manual Dispatch observation', () => { }) ]) - await expect( - call('orchestration.workerShow', { dispatch: dispatch.id }) - ).resolves.toMatchObject({ - worker: { state: 'unsupervised', stage: 'injected', agent_terminal_handle: 'term_worker' }, + const workerShow = (await call('orchestration.workerShow', { + dispatch: dispatch.id + })) as { projection: { liveness: { verdict: string } } | null } + expect(workerShow).toMatchObject({ + worker: { state: 'unsupervised', stage: 'injected', agentTerminalHandle: 'term_worker' }, observation: { status: 'live', exactWorker: true } }) + // Why: worker-show published only PTY liveness, so it read `live` for a dispatch that + // worker-list called `unverifiable` — and worker-list's nextAction sent you back here. + expect(workerShow.projection?.liveness.verdict).toBe( + workerList.workers[0].projection.liveness.verdict + ) await expect( call('orchestration.workerRead', { dispatch: dispatch.id, source: 'terminal' }) ).resolves.toMatchObject({ diff --git a/src/main/runtime/rpc/methods/orchestration-worker-control.ts b/src/main/runtime/rpc/methods/orchestration-worker-control.ts index 9689d5ca0cc..7538237518c 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-control.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-control.ts @@ -6,9 +6,12 @@ import { defineMethod, type RpcMethod } from '../core' import { OptionalFiniteNumber, requiredString } from '../schemas' import { callFederatedWorkerShow, + exposeDispatchContext, exposeFederatedWorkerObservation, + exposeObservation, exposeWorker, inspectWorkerTerminal, + projectFleetWorker, resolvePinnedFederatedServer, showContextOnlyWorker } from './orchestration-worker-observation' @@ -118,8 +121,9 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS: RpcMethod[] = [ ) } return { - dispatch: db.getDispatchContextById(params.dispatch), + dispatch: exposeDispatchContext(db.getDispatchContextById(params.dispatch) ?? dispatch), worker: exposeWorker(worker), + projection: projectFleetWorker(runtime, db, params.dispatch), server: { environmentId: server.environmentId, name: server.name }, remoteRuntimeEpoch: db.getFederatedDispatch(params.dispatch)?.remote_runtime_epoch ?? @@ -148,19 +152,12 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS: RpcMethod[] = [ const observation = await inspectWorkerTerminal(runtime, db, params.dispatch) const resource = db.getWorkerTerminalResourceByOwner(params.dispatch) return { - dispatch, + dispatch: exposeDispatchContext(dispatch), worker: exposeWorker(worker), + // Why: the fleet verdict, so worker-show and worker-list cannot disagree. + projection: projectFleetWorker(runtime, db, params.dispatch), terminal: observation.exact ? observation.terminal : null, - observation: { - status: observation.status, - exactWorker: observation.exact, - // Why: a bare `unverifiable` is not actionable without naming what we lost. - ...(observation.reason ? { reason: observation.reason } : {}), - // Why conditional: a present null must mean "looked, nothing waiting". An - // unattached, missing or identity-changed worker was never looked at, and saying - // null there is the false negative this field exists to remove. - ...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {}) - }, + observation: exposeObservation(observation), terminalResource: resource ? exposeWorkerTerminalResource(resource) : null } } diff --git a/src/main/runtime/rpc/methods/orchestration-worker-list-method.ts b/src/main/runtime/rpc/methods/orchestration-worker-list-method.ts index da5ed905c0a..033f2610b68 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-list-method.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-list-method.ts @@ -272,7 +272,15 @@ async function projectWorkerListPageWithFilteredSnapshot( agentTerminalHandle: row.agentTerminalHandle, terminalState: row.terminalState, resource: row.resource ? exposeWorkerTerminalResource(row.resource) : null, - projection + // Why: `projection.resource` restated id/ownerDispatchId/releaseState/terminalState + // that the row already carries; only the derived ownership classification is new. + projection: { + ...projection, + resource: + projection.resource.state === 'absent' + ? projection.resource + : { state: projection.resource.state } + } } }) const counts = Object.fromEntries( diff --git a/src/main/runtime/rpc/methods/orchestration-worker-observation.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-observation.test.ts index c53572be1ae..df8e24836a0 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-observation.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-observation.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import type { OrcaRuntimeService } from '../../orca-runtime' import type { OrchestrationDb } from '../../orchestration/db' -import { inspectWorkerTerminal } from './orchestration-worker-observation' +import { + exposeDispatchContext, + exposeWorker, + inspectWorkerTerminal +} from './orchestration-worker-observation' +import type { DispatchContextRow, WorkerDispatchRow } from '../../orchestration/types' const DISPATCH_ID = 'ctx-worker' const TERMINAL_HANDLE = 'term-worker' @@ -63,3 +68,58 @@ describe('inspectWorkerTerminal missing liveness verdict', () => { }) }) }) + +describe('worker-show receipt shape', () => { + it('parses the JSON columns once and emits one casing', () => { + const exposed = exposeWorker({ + dispatch_id: DISPATCH_ID, + runtime_epoch: 'epoch-1', + state: 'ready', + stage: 'input_accepted', + worktree_id: 'repo::/tmp/wt', + agent_terminal_handle: TERMINAL_HANDLE, + setup_state: 'ran', + effects: '[{"kind":"setup"}]', + residual_resources: '["res-1"]', + start_options: '{"agent":"codex"}', + last_error: null, + created_at: 'now', + updated_at: 'now' + } as WorkerDispatchRow) + + expect(exposed).toEqual({ + dispatchId: DISPATCH_ID, + runtimeEpoch: 'epoch-1', + state: 'ready', + stage: 'input_accepted', + worktreeId: 'repo::/tmp/wt', + agentTerminalHandle: TERMINAL_HANDLE, + setupState: 'ran', + effects: [{ kind: 'setup' }], + residualResources: ['res-1'], + startOptions: { agent: 'codex' }, + lastError: null, + createdAt: 'now', + updatedAt: 'now' + }) + }) + + it('parses host_scope and withholds authority hashes from the dispatch row', () => { + const exposed = exposeDispatchContext({ + id: DISPATCH_ID, + run_id: 'run-1', + task_id: 'task-1', + launch_token_hash: 'launch-secret', + capability_hash: 'capability-secret', + host_scope: JSON.stringify({ kind: 'local', hostId: 'local' }) + } as DispatchContextRow) + + expect(exposed).toMatchObject({ + id: DISPATCH_ID, + hostScope: { kind: 'local', hostId: 'local' } + }) + expect(exposed).not.toHaveProperty('host_scope') + expect(exposed).not.toHaveProperty('launch_token_hash') + expect(exposed).not.toHaveProperty('capability_hash') + }) +}) diff --git a/src/main/runtime/rpc/methods/orchestration-worker-observation.ts b/src/main/runtime/rpc/methods/orchestration-worker-observation.ts index d10c5071b99..c7386d28eaf 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-observation.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-observation.ts @@ -3,6 +3,8 @@ import type { OrcaRuntimeService } from '../../orca-runtime' import type { OrchestrationDb } from '../../orchestration/db' import { OrchestrationError } from '../../orchestration/orchestration-error' import { parseWorkerTerminalHostScope } from '../../orchestration/worker-terminal-process-liveness' +import type { OrchestrationFleetWorker } from '../../../../shared/orchestration-fleet-projection' +import { projectWorkerFleet } from './orchestration-worker-list-projection' import type { DispatchContextRow, FederatedDispatchRow, @@ -83,24 +85,46 @@ export async function inspectWorkerTerminal( } } +/** Why conditional: a present `agentWait: null` must mean "looked, nothing waiting"; an + * unattached, missing or identity-changed worker was never looked at, and a bare + * `unverifiable` is not actionable without naming what contact was lost. */ +export function exposeObservation(observation: Awaited>) { + return { + status: observation.status, + exactWorker: observation.exact, + ...(observation.reason ? { reason: observation.reason } : {}), + ...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {}) + } +} + export function exposeContextOnlyWorker(dispatch: DispatchContextRow) { return { - dispatch_id: dispatch.id, - runtime_epoch: null, + dispatchId: dispatch.id, + runtimeEpoch: null, state: 'unsupervised' as const, stage: dispatch.capability_hash ? 'injected' : 'context_only', - worktree_id: null, - agent_terminal_handle: dispatch.assignee_handle, - setup_state: 'not_applicable', - effects: [], - residualResources: [], - startOptions: {}, - last_error: dispatch.last_failure, - created_at: dispatch.created_at, - updated_at: dispatch.completed_at ?? dispatch.created_at + worktreeId: null, + agentTerminalHandle: dispatch.assignee_handle, + setupState: 'not_applicable', + effects: [] as unknown[], + residualResources: [] as unknown[], + startOptions: {} as unknown, + lastError: dispatch.last_failure, + createdAt: dispatch.created_at, + updatedAt: dispatch.completed_at ?? dispatch.created_at } } +// Why: `launch_token_hash` and `capability_hash` are authority material with no receipt +// consumer, and `host_scope` shipped as a JSON string inside JSON. One camelCase shape. +export function exposeDispatchContext(dispatch: DispatchContextRow) { + const exposed: Partial & { hostScope?: unknown } = { ...dispatch } + delete exposed.launch_token_hash + delete exposed.capability_hash + delete exposed.host_scope + return { ...exposed, hostScope: parseWorkerTerminalHostScope(dispatch.host_scope) } +} + export async function showContextOnlyWorker( runtime: OrcaRuntimeService, db: OrchestrationDb, @@ -108,28 +132,64 @@ export async function showContextOnlyWorker( ) { const observation = await inspectWorkerTerminal(runtime, db, dispatch.id) return { - dispatch, + dispatch: exposeDispatchContext(dispatch), worker: exposeContextOnlyWorker(dispatch), + projection: projectFleetWorker(runtime, db, dispatch.id), terminal: observation.exact ? observation.terminal : null, - observation: { - status: observation.status, - exactWorker: observation.exact, - ...(observation.reason ? { reason: observation.reason } : {}), - ...(observation.agentWait !== undefined ? { agentWait: observation.agentWait } : {}) - }, + observation: exposeObservation(observation), terminalResource: null } } +// Why: the row was spread verbatim beside its parsed copies, so a reader got +// `residual_resources` (a JSON string) next to `residualResources` (an array) and had to +// guess which was authoritative. Parse once, emit camelCase once. export function exposeWorker(worker: WorkerDispatchRow) { return { - ...worker, + dispatchId: worker.dispatch_id, + runtimeEpoch: worker.runtime_epoch, + state: worker.state, + stage: worker.stage, + worktreeId: worker.worktree_id, + agentTerminalHandle: worker.agent_terminal_handle, + setupState: worker.setup_state, effects: JSON.parse(worker.effects) as unknown[], residualResources: JSON.parse(worker.residual_resources) as unknown[], - startOptions: JSON.parse(worker.start_options) as unknown + startOptions: JSON.parse(worker.start_options) as unknown, + lastError: worker.last_error, + createdAt: worker.created_at, + updatedAt: worker.updated_at } } +/** + * The same fleet verdict `worker-list` publishes, for one Dispatch. + * + * Why worker-show needs it: `observation.status` is PTY liveness, so an agent that died + * at a trust prompt inside a live pane read `live` here and `unverifiable` from + * `worker-list` — and `worker-list`'s own `nextAction` pointed back at this command. + */ +export function projectFleetWorker( + runtime: OrcaRuntimeService, + db: OrchestrationDb, + dispatchId: string +): OrchestrationFleetWorker | null { + const rows = db.listWorkerTerminalResources({ dispatchIds: [dispatchId], limit: 1 }) + if (rows.length === 0) { + return null + } + const now = Date.now() + return ( + projectWorkerFleet({ + rows, + attentionFacts: db.getWorkerAttentionFactsForDispatches([dispatchId], now), + statuses: runtime.getOrchestrationFleetAgentStatusSnapshot(), + limit: 1, + now + }).workers[0] ?? null + ) +} + export function exposeFederatedWorkerObservation( observation: { status?: string; exactWorker: boolean; reason?: string }, projected: boolean diff --git a/src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts b/src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts index 8c37db516cc..361368ca2e2 100644 --- a/src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-workers-recovery.test.ts @@ -290,7 +290,7 @@ describe('orchestration worker recovery', () => { await expect( call('orchestration.workerShow', { dispatch: started.dispatch.id }) ).resolves.toMatchObject({ - worker: { state: 'stopped', stage: 'process_stopped', last_error: null }, + worker: { state: 'stopped', stage: 'process_stopped', lastError: null }, observation: { status: 'exited', exactWorker: true } }) expect(db.getTask(task.id)?.status).toBe('blocked') @@ -370,7 +370,7 @@ describe('orchestration worker recovery', () => { }) await expect(show).resolves.toMatchObject({ - worker: { stage: 'released', agent_terminal_handle: null }, + worker: { stage: 'released', agentTerminalHandle: null }, remoteRuntimeEpoch: 'windows_epoch_new', terminal: null, observation: { diff --git a/src/shared/pty-liveness-verdict.test.ts b/src/shared/pty-liveness-verdict.test.ts new file mode 100644 index 00000000000..48d7afe84c8 --- /dev/null +++ b/src/shared/pty-liveness-verdict.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { describeUnconfirmedAgentStop, describeUnconfirmedStop } from './pty-liveness-verdict' + +describe('unconfirmed-stop sentences', () => { + it('terminates a reason that has no terminator', () => { + expect(describeUnconfirmedStop('its SSH provider is no longer registered')).toBe( + 'The PTY was not confirmed stopped: its SSH provider is no longer registered.' + ) + }) + + it('does not double the terminator on a reason that is already a sentence', () => { + // A relayed lifecycle_conflict message arrives punctuated and printed `...to failed..`. + expect( + describeUnconfirmedAgentStop({ + ptyStopVerdict: 'unverifiable', + ptyStopReason: 'worker w1 cannot transition from stopping to failed.' + }) + ).toBe( + 'The agent terminal was closed but its process could not be confirmed stopped: worker w1 cannot transition from stopping to failed.' + ) + }) + + it('still terminates the live-process wording', () => { + expect(describeUnconfirmedAgentStop({ ptyStopVerdict: 'live' })).toBe( + 'The agent terminal was closed but its process could not be confirmed stopped: it is live.' + ) + }) +}) diff --git a/src/shared/pty-liveness-verdict.ts b/src/shared/pty-liveness-verdict.ts index f16e756ca9a..0dd650161f3 100644 --- a/src/shared/pty-liveness-verdict.ts +++ b/src/shared/pty-liveness-verdict.ts @@ -16,9 +16,15 @@ export const NO_OBSERVING_PROVIDER_REASON = 'no registered provider can observe export const SSH_EXIT_UNCONFIRMED_REASON = 'the owning SSH host did not confirm the PTY exit' export const PTY_LIVE_NOTE = 'The PTY is live.' +// Why: reasons reach these sentences from verdicts, receipts and relayed errors, and +// some already end in a terminator — appending one blindly printed `...to failed..`. +function endSentence(detail: string): string { + return /[.!?]$/u.test(detail.trimEnd()) ? detail.trimEnd() : `${detail.trimEnd()}.` +} + /** The one sentence every surface uses to admit a stop was not confirmed. */ export function describeUnconfirmedStop(reason: string): string { - return `The PTY was not confirmed stopped: ${reason}.` + return `The PTY was not confirmed stopped: ${endSentence(reason)}` } /** Words a close whose PTY teardown was never confirmed, for a stop receipt. */ @@ -30,5 +36,5 @@ export function describeUnconfirmedAgentStop(close: { close.ptyStopVerdict === 'live' ? 'it is live' : (close.ptyStopReason ?? 'the stop outcome could not be verified') - return `The agent terminal was closed but its process could not be confirmed stopped: ${detail}.` + return `The agent terminal was closed but its process could not be confirmed stopped: ${endSentence(detail)}` }