Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions .claude/orchestrator.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@
"workers": {
"claude": {
"command": "claude",
"args": ["--permission-mode", "bypassPermissions"],
"args": [
"--permission-mode",
"bypassPermissions"
],
"models": {
"default": { "model": "opus" },
"cheap": { "model": "sonnet" },
"deep": { "model": "opus", "args": ["--effort", "max"] }
"default": {
"model": "opus"
},
"cheap": {
"model": "sonnet"
},
"deep": {
"model": "opus",
"args": [
"--effort",
"max"
]
}
},
"interactive": true,
"automationBudget": {
Expand All @@ -19,27 +32,40 @@
"cheap": 50000,
"deep": 250000
}
},
"notes": "The prompt is the ticket body (D2). Model routing: the default is opus, tier:cheap selects sonnet, and tier:deep selects opus with --effort max so hard work gets a distinct invocation without changing the no-label default. The legacy worker:sonnet label is rejected loudly with remediation to use tier:cheap. The mode MUST be bypassPermissions, never acceptEdits: acceptEdits auto-approves file writes only, so every shell command still prompts, and a worker with nobody at the keyboard is stuck. Measured on the ORB-75 Phase 7 run, where git switch, dotnet build/test/format, gh and orca were all denied and the worker delivered files with zero gates run, zero commit and zero PR. There is deliberately no -p here either: headless mode is invisible to Orca, so the worktree card shows no Agents row and clicking it reveals a bare shell. The worker runs as a TUI, which means orca terminal wait must use --for tui-idle, never --for exit. One more thing a TUI worker needs answered for it: a brand-new worktree is a folder Claude Code has never seen, so its first screen is the workspace-trust gate (Quick safety check: Is this a project you created or one you trust? with 1. Yes, I trust this folder). Measured on the 2026-07-24 ORB-75 launch, where orca terminal wait --for tui-idle returned satisfied: false with blockedReason codex-trust-workspace and the worker sat on that screen with nobody at the keyboard. Same failure class as the permission mode above: the fix is to send 1 and Enter and wait again, which tools/launch-worker.mjs now does on every launch. Two more things measured on that same run, both about talking to a live worker. First, never orca terminal send to a worker that is not tui-idle: a send issued mid-turn is not delivered as a user turn at all. In the worker's own session transcript it appears only as four type: queue-operation records, never as a type: user entry, and the running turn was cut short on the mid-flow sentence Now the auth-side signup tests., leaving 14 modified and 7 untracked files with zero commits, zero gates and no PR. Everything a worker needs belongs in the prompt FILE at launch; mid-run information is appended to that file and pointed at, which is what tools/nudge-worker.mjs enforces by refusing to send while the terminal is busy. Second, tui-idle means stopped, not done: the same run read satisfied: true as completion when the worker had abandoned the task with nothing committed. Idle is a trigger to run tools/worker-status.mjs, which derives the verdict from commits, push, PR and Linear state, never a report of success."
}
},
"codex": {
"command": "codex",
"args": ["-c", "windows.sandbox=\"unelevated\"", "--dangerously-bypass-approvals-and-sandbox"],
"args": [
"exec",
"-c",
"windows.sandbox=\"unelevated\"",
"--dangerously-bypass-approvals-and-sandbox"
],
"models": {
"default": {
"model": "gpt-5.6-terra",
"args": ["-c", "model_reasoning_effort=\"medium\""]
"args": [
"-c",
"model_reasoning_effort=\"medium\""
]
},
"cheap": {
"model": "gpt-5.6-luna",
"args": ["-c", "model_reasoning_effort=\"low\""]
"args": [
"-c",
"model_reasoning_effort=\"low\""
]
},
"deep": {
"model": "gpt-5.6-sol",
"args": ["-c", "model_reasoning_effort=\"high\""]
"args": [
"-c",
"model_reasoning_effort=\"high\""
]
}
},
"interactive": true,
"interactive": false,
"automationBudget": {
"tier": "routine",
"tokenBudget": 1000000,
Expand All @@ -49,11 +75,11 @@
"cheap": 50000,
"deep": 250000
}
},
"notes": "Model routing: the default is gpt-5.6-terra at medium reasoning, tier:cheap selects gpt-5.6-luna at low reasoning, and tier:deep selects gpt-5.6-sol at high reasoning. The default is based on the 23 pull requests in the 2026-07-28 unattended run: every pull request ran on Sol at high, with a mean of 2.0 CHANGES_REQUESTED rounds, a median of 1, and worst results of 13, 7 and 5. Sol at high therefore averaged exactly the two rounds that had condemned Terra at medium, so the falsifier fired and the decision was reversed by ADR. The legacy worker:sonnet label is rejected loudly with remediation to use tier:cheap. Requires a paid ChatGPT plan plus codex login; a headless session can start that itself with codex login --device-auth, which prints a URL and a one-time code for the account owner to enter. CODEX_HOME DECIDES WHETHER A WORKER IS LOGGED IN, and it is the first thing to check before believing any auth verdict: Orca redirects codex's home for the terminals it spawns, so on this machine the real credential is C:\\Users\\thoma\\AppData\\Roaming\\orca\\codex-runtime-home\\home\\auth.json, not the default ~/.codex/auth.json. Measured 2026-07-27: a shell that had lost CODEX_HOME reported 'Not logged in' and codex doctor reported 'no Codex credentials were found', while the very same CLI in an Orca terminal in the same worktree reported 'Logged in using ChatGPT' minutes earlier. For an unattended worker those two states are indistinguishable from having no plan at all, and the worker dies on a login screen nobody is there to answer. Two consequences. Never diagnose codex auth from an ad hoc shell without printing CODEX_HOME first. And a codex worker that never reaches tui-idle should be suspected of sitting on the login splash: that splash ANIMATES, so the repaint check in tools/launch-worker.mjs now turns it into a loud exit 1 rather than a prompt pointer typed into a sign-in screen. Reads AGENTS.md natively. The invocation is bare codex with no subcommand, which is what codex --help means by 'If no subcommand is specified, options will be forwarded to the interactive CLI'. codex exec is the NON-interactive subcommand and lands in the same unsupervisable place as claude -p, so tools/launch-worker.mjs still refuses it (per-engine headless tokens: exec and its alias e for codex, -p and --print for claude, because codex's -p is --profile, a legitimate interactive flag). Approval and sandbox policy is --dangerously-bypass-approvals-and-sandbox, not the equivalent pair -a never --sandbox danger-full-access, and never --full-auto: --full-auto is -a on-request --sandbox workspace-write, and on-request lets the MODEL decide when to ask a human who is not at the keyboard. The single flag beats the pair because the pair has a half-state: an edit that drops -a never while keeping the sandbox flag silently restores approval prompts to a worker nobody is watching, which is the exact failure this entry exists to prevent. The containment story is the disposable Orca worktree, the same one that justifies claude's bypassPermissions. Everything below was measured on 2026-07-27 against codex-cli 0.145.0 on Windows 11. -c windows.sandbox=\"unelevated\" is load-bearing: Codex's default Windows sandbox is elevated, its setup needs Administrator rights, and the first-run TUI paints 'Set up default sandbox (requires Administrator permissions)' then sits on 'Setting up sandbox... Input disabled until setup completes' forever in a PTY that has no desktop to raise UAC on. Measured still spinning after 2.5 minutes, with orca terminal wait reporting satisfied: true throughout, a false idle. The unelevated fallback (restricted tokens and ACLs, learn.chatgpt.com/docs/windows/windows-sandbox) needs no elevation, and the worker never executes inside it anyway because the bypass flag runs commands unsandboxed. cwd needs no -C: orca terminal create --worktree already starts the TUI in the worktree. Supervision, all measured: a real TUI opens, so the Orca card carries an Agents row and orca terminal wait --for tui-idle has a state to observe. The first screen in a brand-new worktree is codex's own trust gate, 'Do you trust the contents of this directory?', which normally reports blockedReason codex-interactive-prompt. Orca 1.4.156 was also measured retaining codex-trust-workspace on an idle codex terminal that never saw a trust gate. WHY: PR #629 owner adjudication makes that live capture authoritative over the older one-reason-per-engine mapping. It is a preselected list whose own instruction reads 'Press enter to continue', so the answer is Enter ALONE; the 1 then Enter that answers Claude Code's gate left the codex process exited (-1). launch-worker.mjs therefore keeps the trust answer per engine. Never blind-Enter an unrecognised codex screen: a stray Enter meant for one prompt was measured confirming the NEXT one, which is how the Administrator sandbox setup above got selected. Everything the claude entry says about talking to a live worker holds here unchanged: never send to a terminal that is not tui-idle, and tui-idle means stopped, not done, so it is the trigger to run tools/worker-status.mjs, never a report of success."
}
}
},
"maxParallelWorktrees": 4,
"maxSlicesPerWorker": 3,
"attemptsBeforeRewrite": 2,
"linear": {
"team": "ORB",
Expand Down
33 changes: 12 additions & 21 deletions .claude/skills/orchestrate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,26 +302,16 @@ what is unmet. For a `visible-effect` ticket, also inspect the issue evidence an
attached critique paired with the final screenshots before treating the contract as met. That
list plus this critique check is what you nudge with. Nothing else counts as "done".

**Never `terminal send` to a worker that is not idle.** Measured on the same run: a send
issued mid-turn never became a user turn at all. It appears in the worker's session
transcript only as `queue-operation` records, and the running turn was cut short on a
mid-flow sentence. Everything a worker needs belongs in its prompt FILE at launch. When new
information arrives mid-run, append it to that file and point the worker back at it:

```
node tools/nudge-worker.mjs --terminal <handle> --prompt-file <path> < update.md
node tools/nudge-worker.mjs --terminal <handle> --text "<one line>"
```

Either form waits for tui-idle first and REFUSES with exit 1 (sending nothing) while the
worker is busy, so a mid-turn send is not reachable through the sanctioned path.
**Headless workers cannot receive a mid-run user turn.** `codex exec` has no terminal
injection channel. When information arrives mid-run, wait for the worker process to exit,
derive the artifact verdict, update the prompt, and relaunch. Do not promise a nudge that
cannot be delivered.

**What the fleet is doing right now** is `/watch` (`tools/worker-watch.mjs`): per worktree, the
ticket, the branch, the Linear state, BUSY or IDLE by repaint delta, the last meaningful output
lines, and the contract verdict above. Liveness and delivery answer different questions, and
`IDLE + NOT MET` is the pair that costs a run: a worker that stopped on a question with nobody
at the keyboard. Read it instead of hand-running `orca terminal read`, which returns a busy
worker's tail as thousands of characters of concatenated `Working` fragments.
ticket, the branch, the Linear state, BUSY or IDLE from the launcher-written worker PID, and the
contract verdict above. Liveness and delivery answer different questions, and `IDLE + NOT MET` is
the pair that costs a run: a worker process that exited without delivering. Read it instead of
hand-running `orca terminal read`, which for a headless worker shows no live turn at all.

After the PR opens, the worker owns its automated review cycle. The orchestrator does not read
review bodies, author review-round files, or relay findings back to the worker. It waits for one
Expand Down Expand Up @@ -704,9 +694,10 @@ deletion, the orchestrate-skill fix) each ended their turn on "the monitor will
with no live background child. Two of the three prompts already carried a warning against
exactly that, so the subagent-side half alone does not hold. Both halves are the rule:

- **In a prompt whose task includes waiting on CI or a review:** poll in the FOREGROUND,
sleep 60 to 120s per loop, inside your own turn. End the turn only on the goal state or a
genuinely unfixable blocker, and say which one.
- **In a prompt whose task includes waiting on CI or a review:** use one FOREGROUND blocking
`node tools/pr-watch.mjs --repo <owner/name> --pr <number>` invocation without `--once`.
State `yield_time_ms` explicitly at or above the whole expected wait. End the turn only on
the goal state or a genuinely unfixable blocker, and say which one.
- **On any completion notification whose result reads "waiting", "standing by", or "monitor
armed":** read the real PR/CI state yourself and send the agent back to work with it.
Standing by is not progress.
45 changes: 23 additions & 22 deletions .claude/skills/watch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ name: watch
description: >-
Answer "what is every child session doing right now" in one screen. Reads every Orca worktree
in the Orbit repos via tools/worker-watch.mjs and reports, per worker: the Linear ticket, the
branch, the ticket's Linear state, BUSY or IDLE classified by repaint delta, the last
meaningful output lines with TUI repaint noise stripped, and the worker-status.mjs contract
verdict. Read-only: it sends nothing to a worker and moves no ticket. Use when the question is
"is that worker still working", "is anything stuck", "what are my workers doing", or "where
does the fleet stand" during an /orchestrate run.
branch, the ticket's Linear state, BUSY or IDLE from the launcher-written worker PID, and the
worker-status.mjs contract verdict. Read-only: it sends nothing to a worker and moves no
ticket. Use when the question is "is that worker still working", "is anything stuck", "what
are my workers doing", or "where does the fleet stand" during an /orchestrate run.
argument-hint: "[ui|api|landing] (default: every repo)"
effort: low
---
Expand All @@ -25,19 +24,20 @@ question. Never answer it by handing over the `node` line.
node tools/worker-watch.mjs
```

With a repo argument, scope it: `--repo ui|api|landing`. Add `--no-contract` when the question
is purely "is it alive" and the wait for a fetch plus a `gh` call per worktree is not worth it;
add `--lines <n>` when the default eight output lines cut off the thing you are looking for.
With a repo argument, scope it: `--repo ui|api|landing`. Add `--json` for the machine-readable
report, which additionally carries each worker PID and whether it is alive. Those are the only
flags; `--no-contract` and `--lines <n>` were deleted with the repaint sampler.

Run it from the orbit-ui-mobile repo root. It samples liveness over 3 seconds, so it takes a few
seconds plus roughly a second per worktree for the contract verdict. Exit 0 means the report
printed, and that includes "no Orca worktrees" - an empty fleet is a result, not a failure.
Run it from the orbit-ui-mobile repo root. Liveness is a single PID probe, so the only wait is
roughly a second per worktree for the contract verdict. Exit 0 means the report printed, and
that includes "no Orca worktrees" - an empty fleet is a result, not a failure.

## 2. Read the two verdicts, which answer different questions

**BUSY or IDLE is LIVENESS**, measured as repaint delta across two `orca terminal list` samples:
a running turn repaints its spinner continuously, an idle TUI emits nothing at all. It says
whether a turn is running. It says nothing about whether the work is any good or even started.
**BUSY or IDLE is LIVENESS**, measured as the launcher-written worker PID: `launch-worker.mjs`
appends the headless worker's PID to `orbit-worker-pids.jsonl` in that worktree's git directory,
and BUSY means that process is still running. It says whether the worker is alive. It says
nothing about whether the work is any good or even started.

**CONTRACT MET or NOT MET is DELIVERY**, from `worker-status.mjs`, derived from artifacts
(commits above the fetched base, a clean worktree, the branch pushed, a PR open, the issue In
Expand All @@ -53,8 +53,9 @@ The pairs mean different things, and the diagnosis is in the combination:
| IDLE | NOT MET | **stopped early**: it ended a turn on a question, hit a wall, or died. Read its last output lines, then decide |
| BUSY | MET | finishing up after the PR, or drifted past its contract. Read the output lines |

`IDLE + NOT MET` is the one that costs a run, because nobody is at that keyboard. The last
output lines in the report are usually enough to tell a question apart from a crash.
`IDLE + NOT MET` is the one that costs a run: the worker process exited without delivering.
Headless workers take no mid-run turn, so the remedy is always to update the prompt file and
relaunch, never to nudge.

## 3. Answer, then stop

Expand All @@ -63,13 +64,13 @@ Lead with the count and the exceptions, not with a table of everything that is f
```
4 workers: 3 BUSY, 1 IDLE
ORB-88 IDLE NOT MET: commits, pushed, pr-open
last output: "Which of these two approaches do you want?"
-> stopped on a question. Its unmet list is the nudge.
worker PID 24180 exited
-> died or finished early. Its unmet list is the relaunch prompt.
ORB-90 BUSY NOT MET (working, 12 commits)
...
```

Then stop. This skill decides nothing: what to send a stalled worker is `/orchestrate`'s
judgement, and the sanctioned way to send it is `tools/nudge-worker.mjs`, which refuses to
deliver into a busy TUI. Never send to a worker from here, never move a Linear ticket from here,
and never treat IDLE as done.
Then stop. This skill decides nothing: what to do about a stalled worker is `/orchestrate`'s
judgement. A headless worker has no live turn channel at all, so `tools/nudge-worker.mjs`
refuses every invocation; the remedy is an updated prompt file and a relaunch. Never move a
Linear ticket from here, and never treat IDLE as done.
Loading
Loading