Orchestration guide
On this page
- Launching a group
- How it works
- The label handshake
- The task board
- What a row shows, and what the
⌄opens - A description: what the row is
- What order the board is in
- Finding things: search, filters, and folding the tree
- Dependencies — what’s actually startable
- Parent tasks and subtasks
- Sprints — batches of work, in order
- Grounding links — what an agent should read first
- Grounding in the brief — the links reach the agent by themselves
- WIP limits (finish before you start)
- What a row shows, and what the
- The to-do list
- Steering, attention, and audit
- CI watches (agent notifications)
- Lock resources (taking turns on something scarce)
- Cross-workspace channels
- Forking an agent’s session
- Group lifecycle
- Custom agent workflows
- Moving between workflows inside the designer
- Switching a running group to another workflow
- Running a block on another machine:
remote:(not usable yet) - Driving a block over its structured protocol:
driver:(pi only, in this release) - Telling orrerix a block’s cache TTL:
cache_ttl_minutes: - A manager pane — the human’s own interface
- Turning on the merge queue
- The review driver’s
driver:block - Delivery triage: the
triage:block - The plan driver
- Watching a plan drive
- Setting up a cross-model reviewer
- Adding a second lens
- Proposed lessons come with their evidence
- What actually reaches a kickoff from
.orrerix/lessons.md - Watching the merge queue
- Watching a review drive
- Guardrails
- Persistence & restart
- Autonomous mode
- Requirements
Orrerix’s headline feature is a native orchestrator / worker pattern: a long-lived planning agent that manages a small fleet of worker agents, each in its own visible pane, with a reviewer agent per PR and an optional planner that scopes bigger work first. You gatekeep only the final review and merge.
Every agent is a normal CLI in its own pane — panes, not subagents — so you can watch and steer any of them by typing directly, and every inter-agent prompt is delivered by typing into the recipient’s CLI, visible verbatim and captured in an audit log.
Launching a group
- Open a new pane (split or new tab) and pick the Orchestrator + workers kind on the welcome / pane-setup screen.
-
Choose the agent CLI and model per role — orchestrator, worker, reviewer, and planner each get their own CLI (Claude Code or Copilot CLI) and model, so you can mix agent types in one group (e.g. a Claude orchestrator driving Copilot workers). The top Agent select is the group default that seeds every role; override any role you like. Model dropdowns are populated by asking the selected CLI what it offers —
opencode models, which lists the models your configured providers actually expose, for OpenCode;pi --list-models, which lists the models your authed providers offer, for pi; the CLI’s own help for the others — so new models appear automatically, with a custom-entry escape hatch.Orrerix also asks each CLI directly which models this machine and this account can actually run, and what each one supports — the same list your CLI’s own model picker would show. What comes back adds rows to the dropdown (nothing is ever removed), labels each with the CLI’s own name for it, and fills in a line under the control with the model’s description, its reasoning-effort levels, and its context-window size. The effort levels the CLI reports for the selected model then become the ones the thinking level knob offers.
This happens automatically, once, when orrerix starts — there is nothing to click. Detection runs in the background, so a picker you open in the first seconds may show its built-in suggestions for a moment and fill in the real list as the answer arrives. A CLI you install while orrerix is running is not detected until you restart it.
Copilot CLI is the exception on both counts. It has no supported way to list its models — its help no longer enumerates them, and it answers neither of the questions above — so its dropdown is a built-in catalog of the models Copilot offers, kept in orrerix rather than read from your machine.
That list is Copilot’s product catalog, not your account’s, and it can go out of date between orrerix releases. So it can disagree with what you can actually run in both directions: it may offer a model your plan does not include — picking one fails at launch rather than being hidden, which is deliberate, since orrerix would otherwise have to guess your entitlements — and it may omit a model your plan does include, whether because your account has access the general catalog does not list or because the model is newer than your build of orrerix. The custom-entry box is the answer to every one of those cases: type the id and it is used as-is. If your Copilot CLI offers a model this dropdown does not, that is expected rather than a sign it is unavailable to you.
Every other CLI keeps the automatic behaviour described above, and Copilot will too once it gains a way to be asked.
-
Set the repository and the guardrails: max live agents, the cost and recovery limits (idle-kill, max spawns/hour, watchdog stall), and permissions.
A group starts with no workers, and there is no setting for it. The orchestrator opens the ones the work needs once it has read the issue — before that, any number would be a guess, and idle agents cost tokens. Ask it for more (or fewer) at any time by typing into its pane.
The card shows the mark of the agent CLI it is about to launch, beside the title: GitHub’s own Copilot glyph for Copilot CLI, a lettered badge for the CLIs with no licensed mark, and nothing at all for a pane that runs no agent — a terminal, a file explorer, or an SSH connection with no remote CLI chosen. It is drawn in that CLI’s own colour, and it is the same mark the pane header wears once the pane is running.
Thinking level and context window
Beside each role’s CLI and model select, the launcher offers two more per-role knobs: thinking level and context window — three knobs in total (model, thinking level, context) for each of orchestrator, worker, reviewer, and planner. All three default to CLI default — the empty value, which orrerix emits nothing for, so the CLI runs exactly as it would with no flag at all.
- Thinking level sets how hard the model reasons before answering (
low/medium/high/xhigh/max), on Claude Code, pi and Codex. Each of the three accepts every level orrerix offers, by a different route: Claude Code takes a--effortflag, pi a--thinkingflag, and Codex themodel_reasoning_effortkey in the profile orrerix generates for it. Copilot CLI and Gemini CLI grey the control out with a reason shown inline: Copilot’s effort level lives in~/.copilot/settings.jsonwith no flag or environment variable to set it, and orrerix never writes a user’s global settings file to reach it; Gemini’s thinking level is a settings-file key too (modelConfigs.aliases.<alias>.thinkingConfig) — the seam orrerix uses to generate a per-agent Gemini settings file already exists, but the key’s schema needs a live check against a running Gemini CLI, so the knob stays disabled until that’s verified. - Context window widens the model’s context (
1m, the only tier today), on Claude Code only, composed onto the model as the[1m]alias suffix (sonnet[1m]) rather than typed into the model field itself. Copilot’s context control is interactive-only (/contextinside its own session) with no argv or settings equivalent, and Gemini’s window is model-determined, so both grey out too.
A role’s knob greys out for either of two separate reasons, each stated inline as the control’s own hint — never silently ignored:
- the CLI can’t honor the knob at all (the Copilot/Gemini cases above);
- for context specifically, on Claude Code, the selected model has no
[1m]form. The suffix is documented only for thesonnet,opusandopusplanfamilies —haiku,fable,bestanddefaulteach grey out with their own reason (there is nohaiku[1m]orfable[1m];bestanddefaultresolve per account, so there’s no fixed name to append the suffix to). A model id orrerix doesn’t recognize — a full model name it hasn’t seen, or a Bedrock/Vertex/Foundry deployment name — leaves the knob enabled instead: orrerix only disables what it can affirmatively rule out, never what it merely doesn’t know, since on those providers the suffix is exactly how the 1M window gets selected.
A knob that clears both checks is still an entitlement, not a guarantee: opus[1m] is a real, documented alias, but [1m] access is plan- and credit-gated on Claude’s side, so picking it for an account that can’t serve it fails visibly at the CLI, in the pane — orrerix doesn’t pre-judge your account’s entitlements by hiding the option. That’s a different failure from the model gate above: the model gate hides a suffix that has no defined meaning at all, while the entitlement case leaves a meaningful suffix selectable and lets the vendor’s own check decide.
These same two keys are available per block in .orrerix/workflow.yml (effort:/context:) for the advanced orchestrator. Loading the file enforces the closed vocabulary and the per-CLI rule above; the workflow pane goes further and also validates context: against the block’s model:, raising a per-block finding when the two disagree (e.g. model: haiku with context: 1m) — the same model-gate rule the launcher’s select uses, so a hand-edited file can’t drift from what the launcher would show. In the pane’s block form the two controls follow the model as you change it, including a model id you type by hand: pick sonnet over haiku and the context window becomes selectable in the same keystroke, with no need to click away from the block and back. See docs/design/workflows.md and the author-loomux-workflow skill.
Permissions are either Auto (Claude Code’s native auto permission mode plus pre-approved git/gh and orrerix agent tools — recommended) or Accept edits only. Orrerix never uses --dangerously-skip-permissions.
Under Auto, group Copilot agents run in Copilot’s true autopilot mode (--autopilot) — an unattended worker should persist autonomously rather than pause to ask — and orrerix answers the resulting “Enable autopilot mode” consent dialog for them automatically at spawn (your group-level Auto choice is the consent). A lone Copilot pane launched with the Autopilot checkbox on gets the same flags and the same dialog-answering watcher — see getting started.
The launcher warns inline when any selected role’s CLI isn’t installed, and an agent pane that dies with an error stays open so you can read what happened.
Promoting a standalone agent
Sometimes the group starts as a conversation. You open a plain Agent pane to try something out, spend an hour on it, and it turns into work worth orchestrating — at which point launching a separate orchestrator means hand-transferring everything you just worked out into a session that wasn’t there for it.
Instead: right-click the pane’s header → “Promote to orchestrator…”. That pane’s own Claude session is relaunched in place with the orchestrator’s full contract — its tools, its task board and audit log, the git/gh shim, a real group on disk — while keeping the conversation it already has. The prototype context is the orchestrator’s context; nothing is summarized or re-typed.
What the confirm tells you before anything happens:
- The repository is the pane’s own working directory. A pane launched into a worktree keys its group to that worktree, not to the main clone.
- This pane’s current turn is interrupted. Promotion has to stop the running CLI to reopen the session under the new contract, so finish (or don’t mind losing) whatever it is mid-answer.
- Which group you land in is decided when you confirm: a new group for the repo; or the repo’s existing dormant group reattached, inheriting its board, audit history and the roster it was launched with; or a sibling group beside one that’s already live (two orchestrators never share a group). A toast names the group once it resolves.
- The workflow checkbox appears only when the repo declares a
.orrerix/workflow.ymlthat validates, and runs that roster instead of the built-in four roles. If the file is there but broken, you’re told so and there’s no checkbox: a new group runs the built-in roles, the same outcome the launcher warns about inline. Either way — valid file, broken file or no file — a reattached dormant group keeps the roster its own launch approved; which roster a promotion runs depends on the group case as much as on the file.
The item is offered on standalone Claude agent panes. It’s greyed with the reason on an agent pane that can’t be promoted yet — a non-Claude CLI, a pane orrerix hasn’t learned a session id for (send it a prompt first: an agent nobody has spoken to has no conversation to carry over), or a pane with no working directory to make the group’s repo — and it isn’t offered at all on a shell pane, a pane running something that isn’t an agent CLI, or a pane that already belongs to an orchestration group. orrerix also refuses a session that was ever a recorded member of a group, even a long-dormant one: a delegate’s transcript carries a delegate’s contract, and two role contracts in one session is not a thing to seat on purpose. Every refusal says which one it is, and a refused promotion changes nothing at all — the pane keeps running exactly as it was.
If the relaunch itself fails after the old process was stopped (a spawn that doesn’t come up, a bind that times out), orrerix does not quietly start a new session in its place — that would look like success while discarding the very thing you promoted. The group is already durable on disk, so the toast names it and points you at its Resume card in the session browser, which brings the conversation back.
Promotion is human-only, like every other gesture on this menu: no agent can promote a pane, its own or anyone else’s.
How it works
Orrerix hosts a local MCP server; every agent pane in a group connects with its own identity token (--strict-mcp-config, so workers see nothing else). The orchestrator:
- plans work as GitHub issues, labeling ones it owns
agent-managed; - every worker AND reviewer spawn gets its own dedicated git worktree — always. Your main clone is your environment, so neither ever branches, commits, or checks anything out there: a worker’s worktree branch is cut from the repo’s default branch (fetched fresh from origin), never from whatever the primary checkout happens to sit on, so parallel work starts from a clean base without a manual rebase. A branch the orchestrator names that already exists is picked up rather than re-cut — including one that exists only on origin, such as an open PR’s branch whose earlier worktree was cleaned up, which is checked out tracking
origin/<branch>; if that branch does not contain the base it was asked for, the spawn fails and says so rather than handing over a worktree on the wrong history. A reviewer’s own worktree is the same kind of clean scratch space, kept separate so two reviewers (or a reviewer and the orchestrator’s own git traffic) never contend on the same checkout. A reviewer’s worktree isn’t a checkout of the PR it’s reviewing (that branch may already be checked out elsewhere); it fetches the PR’s code in detached-HEAD mode when it needs to run something locally, which never collides with anything. Because it is scratch, it does not outlive its pane: when a reviewer pane ends — killed, closed, crashed, or its lane released by the review driver — orrerix removes that worktree and itsagent/<id>branch (a worker’s worktree is never touched this way, and the branch is kept if it carries a commit no other branch or remote holds). A reviewer resumed later gets a fresh one cut at the same path, so its session still finds its workspace. If git refuses the removal (a file still locked on Windows, say), the pane still ends and the refusal is on the audit log asreviewer-worktree-remove-failed. The orchestrator cannot spawn either into the main clone even if it tried — the MCP tool rejects it outright. (A planner is unaffected: it never gets a worktree at all — see below. For its own mechanical git work, like a rebase or conflict fix with no worker worktree still around, the orchestrator uses a staging worktree of its own, kept separate from your clone the same way.)git stashis repo-wide, not per-worktree — the stash stack lives in the shared.git, so agents in separate worktrees of the same group share one stack and apop/drop/clearby one can destroy another’s WIP; role templates tell agents to commit WIP to their own branch instead of stashing; - delegates via tools that type prompts into the worker’s CLI — you see every instruction verbatim in the pane, can steer any agent by typing yourself, and everything lands in the audit log.
Workers follow the standard flow (branch → implement → tests that test intent → docs → PR) and report back; reviewers post gh pr reviews. For bigger or sprawling work the orchestrator can spawn a planner first — a read-only agent that explores the codebase and posts a structured implementation plan (scope, files, test strategy, risks, and a suggested worker split) as an issue comment, then exits. A planner’s read-only contract is enforced at the CLI level where possible: it never gets a worktree, and its file-editing tools plus git commit/git push are denied.
It posts that plan with the post_issue_comment(issue, body) orrerix tool, not by shelling out to gh. The reason is a limit in the CLI rather than a preference: Claude Code will not match a Bash command longer than 10,000 characters against your permission rules, and it treats newlines in a command as separators between sub-commands, so a plan — thousands of characters, many lines — matched no rule and was denied outright in a read-only pane even with gh fully pre-approved. Passing the plan as a tool argument sidesteps both limits, and works the same way whatever CLI the planner block names. The tool posts comments and nothing else: it cannot label, close, merge, review, or create anything. Every post leaves an issue-comment row in the group’s audit log with the issue, the body size and the outcome — the comment URL when it worked, the error when it did not, so a failed post is visible rather than simply absent. (Anything that returns before gh runs at all writes no row: an empty body, an unusable agent id, an unknown group — all argument validation rather than posts — and also a staging failure, where orrerix cannot write the body file at all. That last one is a real attempt that leaves no trace, and it stays a carve-out rather than a fixed gap because the audit log lives in the same directory the staging write just failed to write into.) It is available to the orchestrator and workers too; reviewers are excluded, since a review is the recorded route to a PR.
One thing worth knowing: GitHub numbers issues and pull requests in a single namespace, so a number that is really a PR posts to that PR’s conversation. It still cannot do anything to it except comment.
What it is pre-approved for, since a planner runs with no human in its pane to approve anything: read-only shell and git, gh (it reads the issue and posts its plan through it), the orrerix tools, and — so it can ground a plan in a vendor’s actual reference docs rather than in recall — WebFetch/WebSearch. That last pair means a planner pane can reach arbitrary hosts, which is worth knowing if you plan on sensitive repositories; to switch it off, add a WebFetch (or WebSearch) entry to permissions.deny in the repository’s own .claude/settings.json — a deny rule there beats anything orrerix pre-approves. Running a build or test command is not pre-approved, and orrerix offers no way to widen that — a planner’s persona allow: patterns are dropped, unconditionally. Your own .claude/settings.json can still add one if you decide to: permission rules merge across scopes rather than override — the same merge rule that makes the WebFetch switch-off above work. So what orrerix denies there is no way to allow, but what it merely leaves out of its allow-list — general Bash, and so cargo check — a repo-level permissions.allow can grant. Absent that, a plan will say when it could not confirm something by running it.
What actually reaches the orchestrator’s pane. A delegate’s report is typed into the orchestrator’s pane — waking it for a turn on the group’s most expensive model — only when it needs the orchestrator to do something. done and blocked do: route the next step, drive the PR, merge, ask you. progress never does, so it is not delivered at all — it is written to the audit log and appended as a note on that agent’s board task, where you see it beside the pane and the orchestrator can read it on demand. Nothing is lost; what goes away is the interrupt. A delegate that needs the orchestrator now for something that is not a status change uses its message channel instead, which always lands.
Every review answers two standing questions as well as reporting what it found. Its body carries a ## Premortem section — two ways this change fails in production that no test in the PR would catch, or an argued none — and where the change touches unbounded input (a file, a transcript, anything off the network or supplied by a user or another agent) one of those two is the resource answer: largest realistic input × how often the code runs × what it allocates or reads per run, naming the size at which memory or IO hurts rather than only where time does. Those are question-generation duties, and they exist because the rest of the process is verification: red-before-green evidence proves the tests somebody already thought to write and is silent about the property nobody conceived of. The orchestrator treats a review that arrives without the section — or with an unargued “none” under it — as an incomplete review rather than an approval, and dispositions a premortem entry that names its trigger like any other finding. A repo that wants more of the question set than that — design alternatives, misuse, operational futures — puts it in its own reviewer persona (see Custom agent workflows).
Everything an agent posts on GitHub has two layers. A PR body, a review, an issue an agent files: a short human layer first — what changed and why, what to look at, how each finding was dispositioned — and below it a collapsed <details> block, headed Agent context — evidence, receipts, instruments, carrying the evidence the process demands of agents but not of you: run ids, red-before-green failure lines, mutation tables, blob hashes, base-and-head figures. Click it open if you want it; skip it if you don’t. Nothing about the evidence gets weaker for being folded — only its position moves — and the standing review sections above stay above the fold, because they are the part a human most needs to read. When a merge takes its commit message from the PR body, it takes the human layer only: git log has no fold.
Agents write for you, by default. Every role that posts reads one writing standard, in every group: a body opens with a summary, what to review and how it was tested; a comment is a few lines; a board description or note is one or two sentences; and an issue is filed only for work that must be tracked — a review nit deferred from a PR is a line in that PR’s disposition comment and a line to you, and small follow-ups in one area share one rolling issue. Everything an agent posts on GitHub ends with one line, — Written by AI (<agent>, <model>) on behalf of the human, below the fold, so a merge commit never carries it. A repo’s own personas can add to this; they do not need to repeat it.
No agent ever merges. Agents open PRs; you merge, after your own review.
After a merge, the orchestrator cleans up. It closes the PR’s leftover scratch PRs, confirms the head branch is gone from GitHub, and removes the worker’s and reviewers’ worktrees and local branches; at quiet moments it sweeps for any a merge left behind — only its own group’s, so a worktree or branch it did not create is named to you, never removed. Workers close their own scratch PRs as they go, and a worker’s report names any it could not close.
Panes are badged by role and group number (ORCH 1 / W 1 / REV 1 / PLAN 1 vs ORCH 2 / W 2) with a per-group accent color, so parallel orchestrations — even on the same repository — pair up at a glance. When the orchestrator spawns an agent it opens that pane in the background: your keyboard focus stays exactly where you were typing.
The label handshake
You can hand the orchestrator work without typing in its pane — just label a groomed GitHub issue. A running orchestrator on the repo polls open issues and pulls any so-labelled onto its board; because the label is durable on GitHub, no orchestrator needs to be running when you label — it’s picked up whenever one next starts on that repo.
| Label | Meaning |
|---|---|
agent-ready | Groomed — start work. The issue is driven to a PR through the normal branch → implement → test → PR flow. |
agent-investigation | Research only. A planner (or the orchestrator itself, for small questions) researches options/feasibility and posts findings or a plan as an issue comment — no code. |
agent-managed | Set by an orchestrator to mark “I own this issue.” Shown read-only in the UI. |
agent-hold | The opposite of a go signal: agents must not start this issue. It’s the veto that matters under full autonomy, where every open issue is otherwise eligible. This is the only label in this table a repo can rename (intake.labels.hold:); do so and your spelling replaces it everywhere, including in the paragraph below and throughout this page. |
You can apply agent-ready / agent-investigation / agent-hold straight from the GitHub issues view — toggle the ready, investigate or hold control on an issue row. If the repo doesn’t have these labels yet, orrerix creates the one you toggle on first use (only these allow-listed labels are ever created).
The task board
The orchestrator pane has a board toggle (Alt+T or the list icon) showing the group’s work queue — status per item, issue/PR links, notes, and priority order. You can add, edit, annotate, reorder, and delete tasks; the orchestrator is notified of your edits and maintains the same board through its tools. Issue and PR chips are clickable and open in your browser.
Statuses: queued, in-progress, review, pr, human-testing, prototype, done, blocked.
What a row shows, and what the ⌄ opens
The board is usually read in a normal-width pane with the UI docked to the left, so a row spends its width on the four things you actually scan for and nothing else:
- The task name and its id. The name is shown on one line, cut at about 80 characters with an ellipsis if it runs longer than that. Nothing is lost: the whole name is on the row’s tooltip, and in full — and editable — when you open the row.
- A small coloured square saying which level the row is: epic, feature, story, task, or none. It only appears on a board that uses levels at all, and hovering it says which one in words.
- The issue / PR chips, still clickable, still opening in your browser.
- Progress — the status control, and on a parent task the
done/totalrollup of what is directly inside it. ⌄, at the right-hand end of the row.
Click anywhere on the name to open or shut the row — you do not have to hit the small ⌄. Anything inside the row that has its own click (the issue and PR chips, the status control, an editor you have open) keeps it.
Opening a row shows, above everything else, the full name and the row’s description. Double-click the full name to rename the task; that is where renaming lives now, because a single click on the compact name is what opens the row.
Everything else is one click away behind that ⌄: the full name, the description, the assignee and session chips, the kind and sprint badges, the ready / all inside done / cleared markers, the ACTIVE badge and the needs a decision / needs a look deep link, the ▶ Start / ✓ Approve / ✎ Changes / ▶ Proceed buttons, the 🔗 ⤵ 🏷 🎯 📎 🗨 buttons, ↩ restore and ✕ delete. It opens in place, under the row’s own name — the terminals underneath never move.
Two things worth knowing about how it behaves:
- A row you open stays open while you work — through the board’s own refreshes and through anything an agent writes to it — until you shut it or close the panel. It is how you are reading the board right now, so it is not saved between sessions the way a folded-up parent is.
- A left accent still says what a row needs from you, whether or not it is open: amber for work only you can advance, a green glow and pulse for an agent working on it right now. Those cost the name no room at all, which is why the badges that repeat them sit behind the
⌄.
A description: what the row is
A task can carry one or two plain sentences saying what it is, for whoever reads the board next — you, a week later, or an agent picking the row up. Open a row and click the description line to write one; Enter saves, Escape discards, and clearing the box removes the description. Half-typed text survives an agent writing to the board underneath you.
It is ordinary text, never formatted, and it is capped at 500 characters on one line. Going over is refused rather than quietly cut, so the last thing you wrote cannot vanish without being mentioned — anything longer than that belongs in a 🗨 note or a 📎 grounding link. Space at the start or end is tidied away, but a line break in the middle is refused rather than closed up, so two sentences are never silently welded into one. Your orchestrator can set one too (upsert_task(description: …)), and read one back with get_task. It is deliberately not on the cheap board listing agents poll: the description is written for a human, and putting it on every row of every read is what made the old board listing too big to read.
The 🗨 notes and 📎 grounding sections are unchanged: they open as full-width blocks below the row, so they never compete with the name, and a section you opened stays open whichever way you fold the row itself. The 🗨 and 📎 buttons that open and shut them are behind the ⌄ like the rest, so a section left open under a folded row is shut by expanding the row again.
What order the board is in
The order you can drag is the priority order — top is next, and the orchestrator reads it that way. Two things are derived from it rather than being part of it:
- Finished work sinks. An item that is
done, with nothing unfinished nested inside it, drops below the live work of its own group (the top-level rows, or its container’s children — see Parent tasks and subtasks), ordered most recently updated first. Nothing about your priority order changes: live items keep exactly the relative order you gave them, and adonecontainer that still holds an open task stays where it is, so live work can never disappear under a finished parent. (“Most recently updated”, not “most recently finished” — anything that touches an item counts, so the orchestrator adding a note to an old finished item lifts it back to the top of the finished group.) - Cleared items drop out until you ask for them — see 📥 clear done below.
Because of the sink, the ▲/▼ buttons move an item one step relative to the items you can see above and below it, skipping whatever finished rows happen to sit between them in the file. On a finished item both arrows are off: its place is derived (most recently updated first), not yours to set — reopen it and it rejoins the priority list.
Board controls:
- ▶ Start on a
queueditem nudges the orchestrator to begin now — it records a human note and delivers a begin work prompt to the orchestrator pane. It deliberately leaves the status atqueued; the orchestrator flips it toin-progresswhen it actually assigns a worker. (If the group is paused, Start is refused with a toast — resume first.) - Merge gate — when an item reaches
prorhuman-testing(the point where only you can decide), the board shows ✓ Approve (marks it done and tells the orchestrator to merge) and ✎ Changes (opens a box for your findings, records them, and reopens the task — see below). Both land as a message in the orchestrator pane, exactly as if you’d typed it. Approve only ever shows forpr/human-testing: once you request changes, the task returns to a working status and Approve disappears with it, so a reopened item can never keep showing a stale “approve” affordance for feedback you already sent back. Note that Approve is your merge gate, not the repo’s — if a custom workflow has its own merge gate armed, Approve is relabeled up front whenever the item carries a PR (e.g. “Approve (won’t merge — gate needs rev-orch/rev-ui/rev-tests)”) so you know before you click, and the tooltip names your options: run the missing reviewers, toggle the workflow off, or merge via the GitHub UI directly. What the label says depends on the PR’s base branch, which the orchestrator records on the task alongside the PR itself:- Base is the default branch (or the orchestrator recorded no base, or orrerix could not resolve the repo’s default branch) — the warning above. Unknown is treated as “assume the default branch”: a board that guessed the other way would quietly downplay a merge straight into
main. - Base is some other branch — a stacked sub-PR into an integration branch, say — the label says so and names it (“Approve (sub-PR into integration/581 — the orchestrator merges it once the gate verdicts land)”). Your Approve grant is the default-branch gate, so it is not what this PR is waiting on.
This narrows the story, not the gate. A custom workflow’s merge gate applies to every merge of a PR wherever it lands, integration branches included, and it is enforced against the base ref orrerix resolves live at merge time — never against what a task says. The recorded base is display metadata the orchestrator writes, so treat it the way you’d treat any other board text: informative, not authoritative. Two ways the label can be wrong-but-harmless, both worth knowing: the orchestrator can record a stale base if it retargets the PR without updating the board, and orrerix reads your repo’s default branch from the clone’s own refs rather than fetching, so a default branch renamed on the remote reads as the old name until something fetches. Either way the worst case is a sentence that misdescribes the PR — no merge is authorized by any of this.
- Base is the default branch (or the orchestrator recorded no base, or orrerix could not resolve the repo’s default branch) — the warning above. Unknown is treated as “assume the default branch”: a board that guessed the other way would quietly downplay a merge straight into
- ▶ Proceed on a
prototypeitem (a demo-gated deliverable awaiting your verdict) promotes it: two-click confirm flips it toin-progress, records your decision, and prompts the orchestrator to take the prototype to a full production build. - 📥 clear done (N) clears every finished item out of the list in one click. Nothing is deleted. Each item keeps its place, its notes and its links in the group’s board file, the action is recorded in the audit log, and it comes straight back:
- 👁 show cleared (N) puts them back on screen (they read dimmer, and each wears a small cleared label saying when you cleared it),
- ↩ on any one of them brings that one back into the working list, and
- ↩ restore all (N), next to the toggle while they are on screen, brings back the lot.
Clearing is your view of your board: no agent can clear an item, and no agent can see that you cleared one — the orchestrator’s own view of the board is byte-for-byte what it was. Reopening a cleared item (moving it off
done) brings it back on its own, so a task the orchestrator picks up again can never stay hidden. Because it is a view action and not a work change, the orchestrator is not interrupted with a notice about it, the same way it isn’t for a reorder. - 🗑 done (N) deletes all
doneitems in one action (two-click confirm). This one is permanent and it does not spare the cleared ones — the count is everydoneitem on the board. - 🗑 selected (N) deletes exactly the rows you tick, by id, in one action.
- ✓ Approve selected (N) approves several merge-gate items at once, using the same tick boxes. The count is only the ticked rows that are actually at the gate — tick a
queuedrow for a later delete and it is simply not part of the approval. One dialog lists exactly what you are about to authorize, with an optional note per row (they ride to the orchestrator attached to their own PR), and that dialog is the confirm: nothing is granted until you click through it. Each PR still gets its own one-time grant, exactly as if you had clicked Approve on each row — what the batch changes is that the orchestrator gets one message naming every approved PR and your notes, instead of one prompt per PR. The batch is all-or-nothing: if a row moved off the merge gate between the board rendering and your click, or two of the rows you ticked point at the same PR (a duplicate filing — one grant can’t be announced as two), the whole batch is refused with a toast and nothing is granted; re-tick and click again. A grant that fails to write (a full disk) likewise stops the action with an error rather than quietly reporting that item as having had no PR.
Items that only you can advance (pr, human-testing, blocked) are highlighted so what’s waiting on you stands out. A working-status item (in-progress/review) is one of two things, and the board makes the difference unmistakable rather than subtle:
- Active — its assignee is an agent that’s actually live right now. The row gets a bold, glowing, gently pulsing treatment and, just after the task id, a “● ACTIVE — <agent id>“ badge naming who is on it. This is deliberately the loudest state on the board. The glow is what says active; the badge sits behind the id rather than in front of it, so an active row’s left edge is exactly where every other row’s is — an item’s indent means one thing only, that it is nested inside another item.
- Idle — the status still says
in-progress/review, but the assignee isn’t a currently-live agent (its pane was killed, or it’s an older session). The row reads as muted, not active — an idle/stalled assignment can never be confused with real live work.
The assignee chip itself carries the same distinction: a live agent gets its own green tint, while a history chip (an assignee that isn’t currently live) reads dimmed and in italics — so an old assignee on a done, reopened, or stalled task never looks like the same agent is still sitting there. done items dim further still, receding behind whatever’s still active.
A row that’s blocked on a human decision or parked for a demo also wears a small marker chip right after its id — the first thing to catch your eye once you know the shape. ❓ needs a decision means a pending question names this row; 👀 needs a look means the row itself sits in prototype or human-testing with no question naming it (a row that’s somehow both shows only the decision chip — it’s the more specific, more blocking ask). Click it to jump straight to that item in the NEEDS-YOU panel (Alt+Q).
Finding things: search, filters, and folding the tree
A long-lived group’s board is mostly history — hundreds of rows, most of them finished — so the board has a control strip under its header for narrowing it down. Everything the strip remembers is remembered per group and survives a restart: which containers you folded up and which filters you armed come back exactly as you left them the next time you open orrerix. It is your view and nothing else — no agent can see it, no agent can set it, and none of it is written to the board file the orchestrator reads.
- ⊟ / ⊞ collapse all / expand all fold or unfold every container in one click. (They appear only on a board that nests something.) The per-row collapse chevron still works the way it always has, and both are now remembered.
- Find in this board… filters by a case-insensitive substring of an item’s title or id, so
authandt-142both work. Escape clears it. - level and status chips filter by level (epic / feature / story / task, plus none for items with no level) and by status. Click chips to add them: within one row of chips they’re an any of these (tick
blockedandprto see both), and the two rows narrow each other (storyandblocked). The level chips are hidden on a board that doesn’t use levels. If your board file has been hand-edited to use a level orrerix doesn’t know, that level gets a chip too, so nothing on the board can become unreachable. - sprint chips filter by sprint — one chip per sprint number your board actually uses (they’re never invented, so a board that jumps from 1 to 5 offers
#1and#5and nothing in between), plus backlog for everything with no sprint at all. They behave like the rows above them: any of these within the row, and they narrow the other rows. The whole row is hidden on a board that doesn’t use sprints. - ❗ needs you (N) is the shortcut for the question the board is usually opened with: show only the items blocked on a decision or parked for a demo — exactly the rows wearing the ❓ / 👀 marker chips. It hides itself when nothing is waiting.
- ✕ clears every filter at once, and the count beside it (“7 of 412”) says how much of the board is on screen.
Two things about filtering a tree that are worth knowing, because they are what keeps the board readable as a tree rather than collapsing it into a flat list of hits:
- A match brings its containers with it. Filter to
storyand you still see the epic and feature each matching story lives in, dimmed, because they are context rather than matches. The reverse doesn’t happen: filtering toepicshows the epics alone, not everything inside them. - A filter reveals matches inside folded containers, and doesn’t disturb your folding. While any filter is armed, containers open up far enough to show what matched, and the collapse controls go quiet (they’d have nothing to do — a container is either holding a match, and has to stay open, or has nothing showing under it anyway). Clear the filter and the board is folded exactly the way you left it.
A container’s done/total chip picks up a dashed outline whenever some of its items are off screen — folded up or filtered out — so a collapsed row still tells you how much is inside it.
Dependencies — what’s actually startable
A task can declare that it waits on other tasks on the same board. The orchestrator sets these when a plan implies ordering (that’s what stops “what’s unblocked right now” from being re-derived from prose after every restart), and you can edit them yourself:
- 🔗 on a row opens a picker of the board’s other tasks — choose one and this task now waits for it.
- A row with links grows a second line: blocked by chips, one per dependency, marked ✓ (that one is
done) or ✗ (it isn’t yet). Hover a chip for the other task’s title and status; ✕ on it removes the link. A red ⚠ chip means the link names no task on this board at all — only reachable by hand-editingtasks.json, and worth removing, because it counts as unmet forever. - see also chips are non-blocking annotations the orchestrator keeps. They are shown read-only here and never affect anything below.
- A
queuedtask still waiting on something dims, so it can’t be read as work anyone could pick up; one whose dependencies are all done is marked ▸ ready, next to ▶ Start. A subtask dims the same way while a task it sits inside is the one still waiting — you can’t start a slice whose feature can’t start — and the chips saying what is holding it are on that container’s own row, which is always visible above it. The ready mark appears only once some task on the board actually declares a dependency — on a board that uses none, every queued item is trivially ready and the badge would say nothing.
Only done satisfies a dependency: an item sitting at pr or human-testing is work you haven’t signed off yet, so anything depending on it keeps waiting. Dependencies never move a status on their own — nothing is auto-flipped to blocked — they only change how the board reads. (blocked stays the status for blockers outside the board.)
Edits are validated where the board is stored, not here, so the rules are the same whether you or the orchestrator made them: a link must name a live task, a task can’t depend on itself, and a cycle is refused with an error naming the loop (t-1 → t-3 → t-2 → t-1) — you’ll see it in the board’s toast, and nothing is written. Deleting a task strips it from every remaining task’s links in the same write, so a delete never leaves a dangling dependency behind.
If an agent edited the same row while you were looking at it, your click is refused rather than quietly overwriting what it wrote — the board is telling you it moved under you. For an edit that names a thing (removing a dependency, adding a grounding link) orrerix simply re-applies what you asked against the row as it now stands, so you usually see the result and never the message. For removing a grounding link it stops and repaints instead: that click names a position in the list, and the list you were shown is no longer the list on the board. Look at the refreshed row and click the ✕ you can now see.
Parent tasks and subtasks
A task can sit inside another task — an Epic or Feature the orchestrator created to hang concrete slices under, following the same shape as Agile hierarchies (Epic → Feature → Story → Task). This is containment, and it’s a different relationship from the dependencies above: a subtask’s container says where it belongs on the board, a dependency says what must finish before it can start. Express ordering with a dependency, not by nesting — nesting a task under another one never means “after it”. What nesting does carry is inherited waiting: a subtask isn’t startable while a container above it still has unfinished dependencies of its own (see below).
Each task can carry a level — epic, feature, story, or task — set by the orchestrator, or by you from the board’s own 🏷 picker. The level is enforced, not a label: an epic sits at the top level and inside nothing, a feature sits inside an epic, a story inside a feature, and a task inside a story. Writes that break that are refused with an error saying which level is missing and how to fix it. A container is still ordinary claimable work like any other task, not a special row.
The task level is optional in the sense that matters: a story with no tasks under it is finished work, not a gap. Break a story down into tasks only when the pieces are worth tracking separately.
A task with no level is exempt from all of that, permanently — it can sit anywhere and contain anything. That’s what a plain board is: if your work has no hierarchy worth describing, never set a level and the rules above are invisible to you. It’s also what every board created before levels were enforced looks like, which is why nothing on an existing board broke: the rules are checked only when a write actually sets a level or moves a task, so an older task whose shape predates them stays fully editable — status, notes, assignee, dependencies, all of it. Only a write that re-states that task’s level or container has to resolve it, and the error tells you both ways: nest it where the level belongs, or clear the level.
The one thing the exemption doesn’t allow is a levelled task inside an unlevelled one — “inside a feature” is a claim about the container, so the container needs the level first.
Task ids show the level a task was created at — e-3 for an epic, f-4 a feature, us-5 a story, t-6 a task or a plain row. The numbers come from one counter shared across all four, so no two tasks ever share a number and a mistyped prefix names nothing rather than someone else’s task. Changing a task’s level later never renames it: everything that points at a task — other tasks’ dependencies, the audit log, an agent’s own notes, your memory — points at that string. On a task whose level changed after it was created, the badge is the truth and the prefix just says where it started.
Three more rules about nesting are backend behavior — they hold however the board reaches you, the UI below included:
- Deleting a container promotes its subtasks rather than deleting or orphaning them: they move up to the nearest container still on the board (or to the top level, if the whole chain above them was deleted in the same action). Nothing under a container you delete disappears with it.
- Nesting can run at most 4 levels deep; a write that would go deeper is refused with an error explaining why, the same way an invalid dependency write is.
- Deleting a container can leave a subtask somewhere the level rules wouldn’t have put it — a feature whose epic you delete ends up at the top level. That’s deliberate: the alternatives are refusing your delete, deleting the work inside, or silently stripping the level off the survivor. The task reads and edits normally; it’s the next time you change its level or container that you’ll be asked to resolve it.
Board controls for nesting:
- A ⤵ nest picker on a row lets you choose which other task it sits inside, or promote it back to the top level. It offers only containers the level rules allow — on a levelled row that is the level directly above it, and on an unlevelled row it’s every other task. “Top level” is offered only where the row’s own level permits it, so a feature is never offered a move its epic-shaped rule would refuse.
- Rows nest visually under their container, indented one step per level, with a collapse chevron on any row that has subtasks — collapsing hides the whole subtree, not just its direct children, so a grandchild is never left stranded above its own container. What you have folded up is remembered per group across restarts, and ⊟ / ⊞ in the strip above the list do the whole board at once — see Finding things.
- The level shows on the row as a badge, so you can see at a glance which rows are containers and at what level. A 🏷 picker next to the nest control lets you set or change it directly — offering only the levels this row could legally take where it sits, plus the clear when that is legal too. It can come back empty, and that’s information rather than a bug: a row inside an unlevelled container has no legal level until the container gets one, and a container holding levelled rows can’t drop its own level while they’re inside it.
- The ▲/▼ priority arrows now move a task among its siblings, not through the whole board: the first subtask of a container has nothing above it to swap with, so its ▲ is greyed out even though there are rows higher up the board. Moving a container moves everything inside it along with it, so re-prioritising a feature never scatters its subtasks.
- A container shows a done/total chip counting its direct subtasks — the same count you’d see if you asked the orchestrator for the board. The chip is outlined while some of those subtasks are off screen (the row is folded up, or a filter cut them), so a collapsed container never hides how much is inside it. A container whose entire subtree is done but whose own status hasn’t caught up gets a nudge badge — it’s a prompt for you, never something that flips the container’s status on its own; only you or the orchestrator ever change a task’s status.
- A subtask whose container was removed by hand-editing the board file, or that otherwise points at nothing valid, renders at the top level with a broken-container marker rather than vanishing — the nesting equivalent of the
⚠missing-dependency chip above.
Readiness climbs the nesting: a task is marked ready only when its own dependencies are all done and every container above it has all of its dependencies done too. A slice inside a feature that can’t start yet isn’t startable either, so it no longer says it is.
Only a container’s dependencies count, never its status. A subtask of a container marked blocked is still ready to start — blocked is for blockers outside the board (a decision you owe, an upstream repo), which says nothing about the work nested inside. If you want that task held too, give it — or its container — a dependency, which is the machine-readable way to say it.
Nesting is still board metadata everywhere it counts: it never affects whether a merge is allowed, and it never blocks the orchestrator from assigning a subtask — readiness is a signal for reading the board, not a lock.
Sprints — batches of work, in order
A task can carry a sprint number: Sprint 1, Sprint 2, and so on. A sprint is a batch, not a calendar — the number replaces the timebox, so there are no start dates, no end dates and no duration anywhere. The point is only to say which work comes first: the orchestrator finishes the current sprint before starting the next, and tasks with no sprint at all — the backlog — sit behind everything that has one.
- Put an item in a sprint yourself with the 🎯 button on its row, or ask the orchestrator to. It’s an ordinary board edit either way, not a privileged one — nothing about a sprint is locked down, and the same picker takes an item back to the backlog.
- An item in a sprint wears a
sprint Nbadge, brightened when N is the current sprint. Items with no sprint wear nothing: the backlog is the absence of a badge, not another one. - The board header carries
sprint 2 — 3/7 donewhenever the board uses sprints at all. It counts everything in that sprint, cleared items included, and clicking it shows only that sprint’s items (click again for the whole board). It’s a shortcut for the sprint chips in the strip, and it arms the number — so when the sprint later completes, the board you come back to is still the one you aimed it at, rather than having silently re-pointed itself at the next sprint while you weren’t looking. - The current sprint is the lowest sprint number that still has unfinished work in it. It isn’t stored anywhere: when a sprint’s last item is done, the next one becomes current by itself, so there’s no marker that can drift out of step with the board. Nothing you click flips it — the ⏭ button beside the header moves items, and the current sprint follows from where the items ended up.
- A blocked item keeps its sprint open. That’s deliberate: a sprint quietly ending because the work left in it looked stuck is exactly the thing you’d want to be told about. It stays current until that item is resolved or moved on.
- Moving unfinished work into the next sprint is always explicit. ⏭ shows you the exact list of items that would move — blocked ones included, since those are precisely what a silent roll-over would sweep up — and asks before writing anything. Confirming moves them one at a time, so each shows up in the audit log on its own and the orchestrator sees the board change. Done items keep the sprint they finished in. The orchestrator does the same thing the same way, and says in its pane which items it moved. Nothing rolls over silently.
Sprint numbers don’t have to be tidy — they needn’t be contiguous and needn’t start at 1, so you can leave gaps for planned work without the board minding.
A sprint changes nothing except the order things get picked up in. It doesn’t make an item startable or unstartable, it doesn’t stop the orchestrator claiming something, and it doesn’t interact with WIP limits. An item outside the current sprint is still perfectly ready to start if its dependencies are met — the sprint says what should come first, not what may happen. And the board itself is never re-sorted: it stays in the order you put it in.
On a board that uses no sprints, nothing changes at all.
Grounding links — what an agent should read first
Beyond dependencies, a task can carry links to the things that govern the work: the requirement it has to satisfy, an acceptance spec, the design note that constrains the approach, a test case that pins the behaviour, a doc it has to keep true — or just a plain link worth reading.
This exists to fix a specific failure. An agent picking up a task otherwise has to rediscover its context from scratch every session — hunting through an issue thread for the requirement, guessing which design note applies — and the real risk isn’t wasted time, it’s missing a relevant requirement entirely. A task that carries its grounding as data hands the next agent what governs the work instead of hoping they find it.
- Each link has a type (requirement, spec, design note, test case, doc, or a plain link), a target, and an optional one-line label to show instead of a bare target.
- A target can be an issue or PR ref (
#123), a file in the repo (docs/design/x.md, a test file), or a URL — the surfaces grounding actually lives on. - The orchestrator records them, typically when it creates the task, and planners record them as part of a plan — so the artifacts a plan names become something the next agent reads rather than prose someone has to re-parse.
Two things links deliberately don’t do. They never affect readiness or ordering — a link is context, not structure; if you mean “this must finish first”, that’s a dependency. And targets are never checked for existence: the board doesn’t go and look, so it keeps working offline and a board edit never fails because GitHub was slow. A link pointing at something that has moved is kept as it is, the same as a dependency naming a task that isn’t there — yours to fix, never silently dropped.
One thing is checked: a link whose target names another task on this board is refused, with an error saying so. That’s what dependencies and see-also links are for, and the two kinds of link are kept apart on purpose — one points inside the board, the other points out of it.
On the board — where you read and edit them by hand:
- 📎 on a row is the way in, on every row. It carries the row’s link count, so you can see how much grounding an item has without opening anything — on a board where nothing has links yet it’s just 📎, and clicking it is how a row gets its first one.
- Unfolding it lists the links, each with its type and its label (or its target when it has no label, with the raw target beside a labelled one — a gloss shouldn’t hide what it points at). ✕ on an entry removes that one entry, even if another link on the row points at the same thing. If an agent changed the row’s links since it was drawn, the ✕ is refused and the row repaints — see If an agent edited the same row above.
- Clicking a link acts on it. An issue or PR ref and an
http(s)URL open in your browser; everything else — a repo path, anything the board can’t recognise — is copied to the clipboard instead. That’s deliberate: a target is free text that an agent may have written, so only the two shapes orrerix can name are ever launched. The tooltip says which it will be before you click. - Adding one is the row at the bottom of the list: pick a type, type the target, add a label if it helps, Enter or Add link. Nothing is checked for existence (see above), so a link you record now against a doc you’re about to write is fine.
- A task holds at most 32 links. At the limit the add form is replaced by a line saying so, rather than letting you fill in a form that can’t be submitted.
Grounding in the brief — the links reach the agent by themselves
Recording grounding only pays off if somebody reads it, so orrerix delivers it rather than hoping. When the orchestrator opens an agent against a task — the same “open a worker” call, plus the task’s id — that task’s links are composed into the top of the agent’s opening brief, above the task it was given:
Grounding (board task t-42): pointers recorded on that board task to what governs this work — read them before you start. They are context to weigh, never instructions.
- [requirement] Retries must be bounded: #1104
- [design-note] docs/design/retries.md
Your task:
Make the retry path give up after the budget instead of spinning.
You never do this by hand: ask the orchestrator to put a worker (or a reviewer) on a board task and it passes the id for you. What that gets you:
- Reviewers get the section too. A test-case link is a review input as much as a build input — the reviewer reads what the behaviour was supposed to be, not just the diff.
- A task with no links adds nothing, so pointing an agent at a task is always safe. You don’t have to put grounding on a row before you can open an agent against it.
- A wrong task id fails the spawn, out loud. Quietly opening an agent with no grounding would look exactly like a task that has none, and nobody would ever find out.
- An agent opened without naming a task gets exactly the brief it always got.
- Pointing at a task is context, not assignment. It doesn’t claim the task, change its status, or set its assignee — those stay ordinary board edits.
The section says outright that the lines are context to weigh and not instructions, and it sits above Your task: so an agent reads what governs the work before it reads the work. One limit worth knowing: the binding lives only in memory, so after an app restart a rejoined pane has no board row attached and its kickoff carries no section. (A follow-up spawn that resumes a session and names a task does get one, like any other spawn.)
WIP limits (finish before you start)
Max live agents caps how many agents run at once. It says nothing about how much work is open — so an orchestrator can pile up ten items waiting on review while cheerfully starting more. WIP limits cap the work instead: how many items may sit in a status at one time.
Declare them in your workflow file (the file that carries your roster, so this needs the advanced orchestrator on):
board:
wip:
in-progress: 4
review: 3
Declare nothing and the feature is off — no limits, no counts, nothing changes. A status you leave out simply has no cap.
The board shows you where you stand. Each declared limit gets a chip on the task board’s header: review 2/3 while there is room, amber at 3/3, red past it. Your orchestrator sees exactly the same numbers when it reads the board.
By default a limit warns rather than refuses. Crossing one lets the write through, tells your orchestrator (“review now holds 4 of a declared 3”), and records it in the audit log. That is usually what you want first: a limit is a guess until you have run under it, and three notices teach you more about the right number than a week of refusals would. When you believe the number, turn it into a refusal:
board:
wip:
review: 3
enforce: true
enforce: true only ever refuses your agents. Your own board edits are never bounced by a limit you set — you are the one resolving the overload, and the board’s authority is yours. They still show up on the chip and still tell the orchestrator, so nothing about the board goes quiet; you just cannot be blocked by your own rule.
A refused write names the limit, how full the status is and which items are in the way, so your orchestrator can finish one rather than retry.
What counts, exactly. A write is judged on the board it produces: a limit fires when a status ends up over its cap and this write is what raised it. So editing an item already sitting in a full status always lands, and so does every move out — a status that has gone over is never stuck. Assigning work (claim) raises in-progress, which is the point.
Only leaf items count: a container in review does not consume a slot, because the work inside it is counted where the work is. That is why nesting an item under another as you move it can be within a limit that the same move without the nesting would exceed — the row you nested it under stops being counted in the same write. It cuts the other way too: un-nesting the last item out of a container makes that container countable again, which can put its status over a cap without anything having changed status at all.
You can cap any status except done — that one is the release valve every other limit depends on, so orrerix refuses a file that tries. You can cap blocked, and it is useful as a warning — but note that enforce: true is one switch for every cap you declare, not one per status. So if you turn enforcement on, a blocked cap becomes a refusal too, and refusing a move to blocked refuses an agent’s report that something is stuck. Under enforce: true, leave blocked uncapped.
A bad value fails the whole file, on purpose — review: 0, a misspelt status (in-porgress), or any key orrerix does not recognise stops the workflow file from loading at all, taking your roster and merge gate with it, and the launcher shows you why. The error names the statuses you could have written. That is deliberate: a repo that wrote review: 0 believes something about how its board paces, and quietly substituting a default would leave that belief in place while the behaviour went the other way.
The to-do list
Your agents can read and groom your own To-Do list — the personal one in orrerix’s To-Do pane, not the task board above. The two are unrelated: a board task is a group’s work, a to-do is yours, and nothing an agent does to one touches the other.
Every agent except a standalone pane has these seven tools:
| tool | what it does |
|---|---|
todo_list | Read a list: compact rows (title, status, due date, priority, tags, 2/5 steps, who last touched it). Completed items are left out unless asked for; query filters on the title and notes. |
todo_get | One item in full — notes, steps, dates, and the rev an edit should quote. |
todo_add | Add an item: title, notes, due date, reminder, priority, important, tags, steps. |
todo_update | Edit one. Every field is optional and an omitted one is left alone. |
todo_complete | Tick it off, or un-tick it. Steps are untouched. |
todo_delete | Soft-delete: it disappears from every view and the record survives 30 days. |
todo_restore | Put back something it deleted. Their instructions say a row you deleted stays deleted unless you ask for it. |
Which list. scope is global — the one list that follows you everywhere — or workspace, this project’s list, and workspace is the default. An agent cannot name a project: the workspace is worked out from its own group’s repo, so a group running on one repo can neither read nor write another project’s list. An id it cannot see comes back as unknown todo, worded exactly as an id that never existed, so it cannot go fishing for what is on your other lists.
What agents are told to do with it. Their instructions say: read before adding, so you get an updated item rather than a second row saying the same thing; quote the item’s rev when editing something they did not create, so a write of theirs is refused rather than silently replacing an edit of yours; complete something only when they know it is done; and delete one item at a time and only when asked. Grooming your list is welcome — sweeping it is not.
Every item says who touched it. The pane shows the agent that added or last changed a row, so an agent-authored to-do is never mistaken for one of yours.
Caps refuse rather than truncate — 500-character title, 20 KB notes, 20 tags, 100 steps, 5,000 live items per list. A refusal is something the agent can act on; a silently shortened title is a loss you would find weeks later.
Everything is in the audit log. Writes land as todo-add / todo-update / todo-complete / todo-delete on the group whose agent made them — so an agent’s edit to your global list is still findable in that group’s audit — and anything refused lands as todo-refused with the reason. A runaway agent hitting a cap over and over is visible there.
Steering, attention, and audit
These deserve their own detail — see:
- Steering & attachments — the collision-proof compose strip under an orchestrator pane (
Alt+P), and pasting screenshots into a message. - Attention routing — a pane earns a pulsing needs-attention chip when an agent is parked on a prompt only you can answer, when a worker reports done or blocked, when a task hits a human merge gate, or when the orchestrator (or a liaison pane, if the group’s workflow has one) has a question pending your answer. Hovering the question chip shows a live count; clicking it focuses the pane and acknowledges the chip — acknowledging only tells orrerix you’ve seen it, so a chip that’s still genuinely true comes right back on the next scan.
- Provider limits. When the account behind a pane’s model runs out of budget — an OpenRouter key at its spend cap, Claude usage credits exhausted — the CLI prints the provider’s refusal and simply stops. Nothing else about that pane looks wrong, so orrerix reads the refusal off the pane’s own text and raises a red ⛔ provider limit chip naming the provider, how many panes it stopped, and what clears it (add credits, raise the key’s limit, or point the block at a different
model:). Because one limit stops every pane on that provider at once, you get one chip per provider per group rather than one per stopped pane. It is the only URGENT chip nothing you type in the terminal can clear (the amber gate chip is not terminal-clearable either — it comes down when the task moves on the board) — and because it is read off what is currently on the pane, it stays up until that pane runs again and its output scrolls the refusal away. So it comes down when you have fixed the billing and nudged the pane, which is the honest reading: topping up an account does not by itself restart an agent that already stopped. - The NEEDS-YOU panel (
Alt+Q, or the raised-hand icon in an orchestrator pane’s header) is everything currently waiting on you, in one list, badged with a running total in its own header. The newest ask is at the top, and anything raised as urgent is pinned above the rest — so what just arrived is never at the bottom of a scroll. Two kinds of card share that list:- Questions — one card per pending
ask_humanquestion. Pick one of its options (each can carry the asker’s own reasoning under the label), type a free-text answer, or both; a question decides for itself whether it allows a single pick, several, or no free text at all, and the card only offers what that question allows. Sending delivers your answer to the orchestrator’s pane — even for a question a liaison pane posed, since the liaison never gets the answer notice itself and instead reads the outcome back through its ownlist_questions. A card that names the board row it’s holding up links straight to it. - Needs-you items — a demo parked for you to go run, or a request for feedback on a direction. An item is its own record: it says who raised it, when, and what they want back, and it links a board row rather than being one. When it links a row, the card shows that row’s live state — the worktree path where the demo lives (click to copy), a link to its PR when it has one, and its current status; a row with no recorded path says so rather than guessing one. Proceed promotes a
prototype(the same gesture as the board’s own Proceed button). Feedback sends your notes back to the orchestrator — onhuman-testingit’s the request-changes gesture and reopens the task, onprototypeit’s a plain note that leaves the demo gate exactly where it was.
A demo item appears by itself when the orchestrator parks a task in
prototypeorhuman-testing, and settles by itself when that task moves on — you never have to keep the two in step. If the linked row is gone (pruned, or renamed), the card says so and stays clearable rather than disappearing with the ask still outstanding.Resolve on an item’s card is your close-out: I have seen this. It clears the row from the panel and leaves the task exactly where it is — resolving is not a board move, and Proceed/Feedback stay the board actions they always were. You can attach a note, which goes to the orchestrator’s pane; resolving without one just tidies your queue quietly.
Dismiss is on every open card, question and item alike: this no longer matters. You can add a one-line reason (up to 500 characters, or leave it empty). It clears the row without answering it and without moving any task — and unlike a note-less Resolve, it always tells the orchestrator, in a notice that says in words that nothing was decided, so it releases whatever the row was holding instead of reading a decision into your click. Un-blocking the board row a dismissed question cited is then the orchestrator’s call, not something that happens behind it; it re-asks only if it still needs the decision. Dismissing an item also tells the raiser not to raise that ask again. Nothing is deleted: a dismissed row settles as
dismissed(a question) ordismissed:webview(an item), which is a different fact from answered, from withdrawn and from I looked, and stays readable as one for ever.Settled rows — answered, withdrawn or dismissed questions, resolved items — fall into a faded tail (the ten most recent) instead of vanishing. Clear completed in the header hides that tail. It deletes nothing: the rows stay on disk for the audit trail, the choice survives a restart, and nothing still open can be touched by it, which is why it doesn’t ask you to confirm.
The panel is the only place a question gets answered, the only place an item gets resolved, and the only place either gets dismissed — no agent can do any of the three, by any path. Withdrawing stays the separate, agent-side path it always was, and there is one of those for each tier:
withdraw_questionsettles an overtaken question,withdraw_attentionan overtaken item, each of them visibly withdrawn rather than answered, seen or dismissed.The orchestrator’s own side of the item tier is three tools.
request_attentionraises one,withdraw_attentiontakes one back, andlist_needs_youis how a pane re-reads what it has left with you after a compaction or a restart — which is why a demo you parked days ago is still accounted for by an orchestrator that has long since forgotten the conversation. Parking a task already raises its demo item, so a well-behaved orchestrator reaches forrequest_attentionmainly to ask for feedback, the one kind nothing on the board raises for it. - Questions — one card per pending
- orrerix’s protocol is that no agent asks you through a blocking dialog. The orchestrator’s role instructions — and a liaison pane’s, where the group’s workflow declares one — call for filing every question through
ask_humaninstead of a CLI’s own interactive-question dialog, because a dialog holds the whole pane and refuses every delivery queued behind it. That is instruction an agent follows, not yet something orrerix enforces structurally on every CLI, so treat it as the norm rather than a hard guarantee. - An optional per-group desktop notification toggle (🔔 in the lifecycle panel) raises an OS toast the first time a question needs your answer (off by default). It fires for that and for the other reasons above — except a task merely reaching a merge or demo gate, which is common enough (every PR does it) that toasting on it would be noise; the board and the NEEDS-YOU panel are where you see those.
- Most chips clear themselves: they’re recomputed every few seconds, and clicking one focuses the pane and acknowledges it.
- The red ⚠ stuck prompt chip is the exception. It means a prompt orrerix sent was never submitted, and it stays up until orrerix has evidence the pane is fine again. orrerix keeps looking for that evidence for as long as the chip is up — every half-minute or so it re-reads the pane, even long after the delivery that raised the chip has been given up on. If the prompt has left the box and you have typed in that pane since, the chip comes down on its own; if the prompt is still sitting there, or orrerix cannot read enough of the pane to tell, the chip stays, because taking it down would be the one mistake it exists to prevent.
- Some chips have no reading that could ever release them — one reporting messages that were discarded while the group was paused is about something that already happened. So that chip carries its own ✕ dismiss button beside it. Clicking ✕ takes the chip down for good, whatever raised it.
- ✕ takes the chip down and nothing else. It does not unstick the pane, and orrerix never presses Enter on your behalf because you dismissed something. If the prompt really is still sitting unsubmitted in that pane’s input box, it still is afterwards — so if you’re not sure, look at the pane first (hover the chip: it says what it thinks is blocking). Every dismissal is written to the group’s audit log with what was dismissed and how long it had been up, so a chip you cleared can always be looked up later.
- The ⛔ held on a dialog chip is the most urgent of all — it outranks even a worker reporting blocked. It only ever appears on the orchestrator’s own pane, and only while orrerix is actively trying to deliver something to it right now and finding its CLI’s own interactive-question dialog on screen: on Claude that dialog is denied outright (see below), but not every CLI orrerix supports can be told to refuse it at that level, so this chip is the fallback that still tells you a held orchestrator pane is stranding every delegate’s report queued behind it, not just its own. It comes down the moment that delivery attempt stops holding — either because the dialog cleared, or because the attempt gave up waiting (orrerix tries again shortly after, and the chip returns then if the dialog is still there) — so it never needs a dismiss, but it can also briefly drop even while the dialog is still up between one attempt ending and the next one starting.
- ⛔ dialog is a different dialog chip about a different dialog: not the orchestrator’s delivery pipe (the chip above) but a driven pane’s own question — a choice, confirmation or text prompt its CLI raised over the structured protocol, which blocks that pane until it’s answered. The pane wears this red chip while it waits — red for the same reason a stuck prompt is red: nothing the pane can do on its own will un-wedge it. NEEDS-YOU only tells you the dialog is parked; the answer itself goes through the pane’s own dialog card — the
answer_pane_uiboundary, the trusted path a button in your own window takes (typing into a driven pane is refused: it has no terminal). - The orchestrator can’t use its CLI’s own question dialog at all, on Claude. A single stuck question dialog on the orchestrator’s pane once held a whole run overnight — the in-flight workers finished their PRs, and then nothing was reviewed, dispatched or merged until morning, because every delivery to a held pane queues instead of landing (and that queue is bounded: enough of them and further ones are refused outright). So on Claude, the orchestrator (and a human-interface/liaison agent, if your workflow has one) is launched with that dialog denied outright; a delegate pane is unaffected, since a human answering its dialog in person never stalls anyone else.
- Provider limits. When the account behind a pane’s model runs out of budget — an OpenRouter key at its spend cap, Claude usage credits exhausted — the CLI prints the provider’s refusal and simply stops. Nothing else about that pane looks wrong, so orrerix reads the refusal off the pane’s own text and raises a red ⛔ provider limit chip naming the provider, how many panes it stopped, and what clears it (add credits, raise the key’s limit, or point the block at a different
- Audit viewer (
Alt+Aor the history icon) — opens the group’saudit.jsonlas a filterable, searchable timeline: every prompt, spawn, task edit, delivery outcome, and state write, one row each. A follow button live-tails new lines. - Progress timeline (
Alt+Wor the timeline icon) — the same audit data plus GitHub issue/PR lifecycle, plotted on a time axis instead of listed: lanes for group, agents, work, gates and GitHub, a window filter (12 hours by default), and click-through to the raw record. Read-only, and it states its own coverage boundaries — what it is not showing is printed under the chart rather than left to look like quiet. - Token charts (
Alt+Kor the column-chart icon) — what this group has spent, and on what: tokens over time with one line per block and CLI, and a stacked bar per feature on your board, with a labelled mark wherever the fleet’s configuration changed. Read-only, and honest about its own limits the same way the timeline is. Full description below. - Delivery queue. If a pane is busy — an interactive question on screen, or a human’s own line still sitting in its input box — orrerix holds a prompt delivery rather than typing over it. A hold that never clears queues the prompt instead of dropping it: nothing is lost, there’s no timeout (you might be away for hours), and the queue drains automatically, in order, the moment the pane is free again — the first thing it delivers is a one-line summary of what was waiting. One exception to “no timeout”, and it only ever moves a queue forward: if the hold is the interactive-question one, and after 15 minutes the pane’s own screen still shows its ordinary input box, orrerix stops believing its own question detection and pastes anyway (you’ll have had the “stuck behind a question” badge for five minutes by then). It does not stop checking before it presses Enter — if a real dialog is up at that moment the Enter is still withheld and the text is left in the box for you, rather than answering the dialog for you. And a hold on your own typed input is never overridden, at any age. The grey suggested next prompt Claude Code shows in an idle input box is not your typing, and it is not a question either: a pane showing only that suggestion takes deliveries normally. You’ll never see “held … re-send” — if a prompt is safely queued, the notice says so explicitly and tells you not to re-send it (that would just create a duplicate); a payload is only ever reported gone if the notice says DROPPED (the pane’s queue was already full, or the agent’s pane closed while entries were still waiting). The orchestrator’s own pane is the one case orrerix can’t announce this way — a prompt about that pane’s blocked delivery would queue behind the very block it reports — so those notices ride back to the orchestrator on the result of its next tool call instead, and you’ll see them in the audit viewer either way.
- Queue depth, on the pane header. A pane with prompts waiting wears a chip saying how many and for how long —
⇥ 3/8 queued · 12s: three waiting out of a maximum of eight, the oldest queued twelve seconds ago. It appears as soon as anything is waiting and disappears when the queue drains, so “deliveries are flowing” is something you can see rather than infer from the absence of warnings. Once nothing has been delivered to that pane for a minute the chip turns amber and says stalled — that is the one to act on: check the pane for a question waiting on an answer, or your own half-typed line still in its input box; releasing either drains the backlog. Everything you need is in the chip itself, and hovering only adds a sentence. A minimized pane shows the same count on its dock chip (⇥3, amber when stalled), which matters because worker panes open minimized by default — the queues that back up are usually the ones whose header you can’t see. - What a full pane refused. A pane that hit its cap and turned deliveries away is told what it turned away, the moment its queue drains back below the cap: one line naming each refused delivery’s sender, a short preview, and why it was refused. Deliveries whose sender has since got them through are marked as re-sent rather than dropped from the list — the receiving pane can’t tell the difference from the outside, and a list that quietly omitted them would read as “these are all still missing”. Orrerix never re-sends them itself: the senders were told at the time, so the roster names who to ask. It’s bounded (the newest few, with the rest counted and left in the audit log), fires once per drain rather than once per delivery, and obeys the same 8-deep cap it’s reporting on — so it can never pile up behind the backlog it describes. The orchestrator’s own pane gets it the same way as the notices above: riding back on its next tool call.
Token charts
Alt+K, or the column-chart icon in an orchestration pane’s header. Two pictures on one panel, answering what has this group spent, and on what — and, once you have changed something, did that change help.
Tokens over time. One line per block and CLI — worker-std/pi, rev-lead/claude, and so on. Colour is the block; the line style is the CLI, so one block running two different CLIs is two distinguishable lines rather than one averaged one. The chips above the plot pick the counter: all tokens, input, output, cache read, cache write, or cost in dollars. merge CLIs collapses each block’s split back into a single line, and changes nothing else — it is a regrouping, not a filter, so the totals stay put. split by model is the same kind of regrouping the other way: each line splits again by the model its agents were on (worker-std/pi/glm-5), so an orchestrator switched from one model to another mid-session shows up as two lines with each model’s spend on its own. The two chips combine — merge CLIs and split by model groups by block and model. Lines of one block and CLI on different models share a colour and line style; the legend names each one.
There are eight block colours. They go to the blocks that have spent, in the order each one started spending, so a block keeps its colour as the chart grows and a block retired before the chart’s history began never uses one. A colour is kept for the life of the group, so the ninth block ever to spend draws in neutral grey — never a reused colour — even when the first eight are quiet. The legend always names every line.
Tokens per feature. A stacked bar per feature on your task board, split by the same block/CLI keys and in the same order on every bar, so two features can be compared segment by segment. Which feature an agent’s spend lands on is decided by the first of these that answers: a board row assigned to that agent; a row carrying its session; an issue or PR number named in its brief that a board row also carries. Whatever that finds, the chart then walks up the board to the feature the row sits inside.
Change marks. Whenever the repo’s agent-facing configuration moves — your workflow file, a persona, a skill, CLAUDE.md, .orrerix/lessons.md, or the loomux version itself — a dashed vertical is stamped on the plot and labelled with what changed. Where the change was a roster switch, the label names it directly (worker-std: opencode → pi), read from the CLI each block was actually sampled with either side of the mark rather than from the configuration file, so it reflects what the fleet really ran. A model switch gets a mark of its own, per agent session, at the first sample taken on the new model (orchestrator/claude: claude-fable-5-1 → claude-opus-5-5). Effort changes are not marked: the usage record does not carry effort.
Explore the plot. The default counter is output tokens, so cache reads do not flatten the other series; the selected counter and all-token feature bars are labelled distinctly. Over the plot area, the wheel zooms about the pointer and dragging pans (over the axis labels, or anywhere else in the panel, the wheel still scrolls). Hovering draws a crosshair and a readout of every line’s value at that time; over a change mark the readout also says what changed. The zoomed window survives refreshes while following; select custom · reset to return to the default preset. The log y switch and the y-axis autoscale to the visible data.
Trends. Directly under the chart, one small plot per derived metric, on the same window as the chart, so zooming or panning the chart narrows them too: tokens per completed item (a running figure: tokens spent so far in the window ÷ items completed so far), items completed per bucket, mean time-to-completion, and average total tokens per pane. Each plot labels its scale and window, and says how many completed items or samples it rests on. A trend with nothing to plot says why (“no items completed in this window”) rather than showing an empty strip.
The group scorecard. One short table for the window: tokens per completed item, cost per completed item, items done per day, median and mean time-to-completion, review rounds per PR, CI attempts per PR, and each role’s share of the tokens (orchestrator, worker, reviewer, …). Every row shows its n. A figure that cannot be measured reads n/a, never zero. Select a change mark and the table gains before and after columns, each with its own n. Figures from the audit log only see the rows still in the audit window, and the panel says when that history may be truncated.
Breakdown. The per-pane, per-block, per-model and per-work-item averages, the role split, time in each status, and the per-PR review-round and CI lists sit under one Breakdown disclosure below the feature bars, closed until you open it. The per-lane scorecard is inside it too.
Click a mark and the chart zooms to the hour either side of it (fit ±1h in the readout puts it back there after you pan away), with the before/after table: mean spend per five-minute bucket for the hour before against the hour after, per series and in total. That is the “did this help?” number. It reads n/a, never zero, when there is not a full hour on one side — the chart will not invent a comparison it cannot make.
The lane scorecard (inside Breakdown): one row per block and CLI that carried a review lane in the audit window (rev-std/pi, rev-final/claude, …), with four columns: how many PRs that lane was credited on, the median rounds to first pass, the median fail rate (fails ÷ decided verdicts, per PR, then the median of those), and the median wall-clock in hours — first audit row naming a PR to the last rd-* row carrying it. The arithmetic is the same script’s that produced the #2011 comparison table (medians, never totals — different PRs are different work), and a column with fewer than three PRs behind it reads n/a (n=…) rather than a number: two points is an average of a pair, and one is an anecdote. A lane whose CLI cannot be resolved to exactly one — two clis on one PR, or a delegate the roster does not name — is left off the table and listed in the caption underneath, never guessed from the block’s declared CLI. Everything here is computed from the audit log the pane already shows, so it sees the same window the audit viewer does.
Under the table, a coverage caption states whether the read is complete: a PR credited with a delegate whose agent-spawn row did not survive the read has provably lost rows (the log rotated them away), and its counters — here and everywhere else in this panel — are a lower bound. When no window is missing a spawn row, the caption says that too, so “nothing to report” is a claim you can check rather than an absence you have to trust.
What it is deliberately honest about
All of these are printed under the charts rather than left for you to discover:
- An orchestrator’s spend is group-wide, and is never charged to a feature. A long-lived orchestrator session has no per-turn PR attribution, so billing its whole lifetime to whichever task it happened to be holding would be a made-up number. It gets its own bar and its own line, where its real spend is visible.
- Spend the chart cannot place goes to
(unattributed)— the first bar, and never hidden. The legend prints the identity the whole chart is checkable by: features + orchestrator + unattributed = total, and the total says its own scope (total (24h),total (all time)). Those three always add up, at whatever window you have selected. Comparing them with the group panel’s lifetime figure only makes sense under theallpreset — every other window is a slice, so a smaller number there is the window doing its job, not a broken chart. - History starts when this feature was deployed. The series is sampled from what loomux already computes each tick; nothing is rebuilt from your agents’ transcripts. So the panel prints the instant its history begins instead of drawing a flat line where there is simply no data.
- A quiet stretch draws along the floor, not as a slope. Joining two distant samples with a straight line would show an hour of steady spend that never happened — and would look more plausible than the truth.
- A counter that goes backwards is clamped and counted, not drawn as a cliff. That happens routinely when an agent’s transcript rotates.
- Costs say whether they were reported or estimated. A dollar figure with no provenance is claiming a precision it has not got, and a bar whose cost cannot be known reads n/a rather than
$0.00. - The scorecard’s counts are a lower bound over the audit window. The pane reads at most a few thousand rows of a rotating log, so “2 review rounds” for an old PR may mean “2 that survived”. The coverage caption names every window that provably lost rows, and the audit-floor note in the list below names the window’s start.
Follow re-reads every 30 seconds. The underlying series only advances every five minutes, so there is nothing to gain by looking harder.
CI watches (agent notifications)
Distinct from the 🔔 desktop-notification toggle above — that raises a toast for you; this notice goes to the agent, typed into its own pane. Agents don’t sit watching a PR’s CI: the orchestrator, workers, and reviewers can register a background watch — a PR’s checks, or a specific GitHub Actions run — and go do other work; orrerix polls in the background (every 30s) and types an [orrerix] PR #241 checks: SUCCESS — … (watch n-3)-style notice into the registering agent’s own pane the moment it resolves, expires, or fails repeatedly. A watch is capped (4 per agent / 12 per group) and time-bounded (5–240 min, default 60). Pausing a group freezes a watch entirely (no polling, firing, or expiry) until you resume it.
Watches live only in memory, and the two ways an agent loses track of one are different: a /compact drops the agent’s memory of a watch (the watch itself is still registered and still live), so it re-lists to recover what it was waiting on; closing orrerix drops the watch itself — the registry is empty on the next launch — so it must be re-registered from scratch, not merely re-listed.
Where you see it. A watch is visible from your side too, not just the agent’s. The group lifecycle panel (Alt+O) shows a ⏳ waiting on PR #241 checks (expires in 43 min) line under any agent holding one — the reason a worker sitting quietly is waiting on CI, not stuck. The audit viewer (Alt+A) has a one-line sentence for each of a watch’s six lifecycle events (registered, fired, expired, failed, cancelled, cleaned up on agent exit) instead of raw JSON.
The internal watchdog stall check knows about a live watch too: an agent silent past its group’s stall window while it holds one is not nudged to the orchestrator — it’s plausibly waiting on its own CI check, not stuck — and the suppression itself is audited (watchdog-suppressed, carrying a why) so it stays diagnosable. Once that watch resolves (fires, expires, or is cancelled), the agent earns a fresh full stall window from that moment; only silence through that window is treated as a real stall and nudges the orchestrator (watchdog-stall).
Two more stalls are suppressed the same way, for the same reason — the notice would carry no decision. A pane something in this process has already asked to end (an orchestrator kill, an idle reap, a driver release) is on its way out, so its silence is expected (why: exit-initiated); and a reviewer lane a live review drive owns is already being watched by the driver, which answers a stuck lane with a hold that names the PR and what to do about it (why: driven-lane). A stalled agent that is none of these behaves exactly as before, and every suppression is one audit row per stall, not one per tick.
A drive-owned suppression ends when the drive does. However the drive goes away — cancelled, completed, or its driver dying — the next watchdog pass notices that nothing owns the lane any more, records watchdog-rearmed, and gives that pane a fresh full stall window from that moment. So a lane that really is stuck is silent-but-unreported only while a drive is actually watching it, never indefinitely; and because the window is fresh rather than resumed, the re-arm itself never fires a nudge about silence that elapsed while the drive was still in charge.
The suppression is bounded by the watch’s own TTL (5–240 min, default 60 — see “capped … and time-bounded” above), never open-ended: a genuinely hung agent holding a watch is silent-but-unreported for at most that TTL plus one more stall window before the orchestrator gets a notice, and watchdog-suppressed audit lines mark the wait the whole time.
When a notice can’t get in. An [orrerix] notice is typed into the agent’s pane, and a pane that is mid-turn can’t take one — so if an agent blocks its own turn waiting on the very thing the notice would tell it about, the notice sits in that pane’s queue and nothing clears. Orrerix watches for exactly that shape: a pane that has accepted nothing for ten minutes while holding one of orrerix’s own notices gets an [orrerix] notice undeliverable 10 min: … — pane mid-turn … sent to the group’s orchestrator (and to the audit viewer as notice-undeliverable), alongside the ⏸ held chip and the needs-attention badge you’d see anyway. The message says which of the three it looked like — a pane mid-turn, a human’s own line in the box, or a dialog waiting for an answer — because those need different things from you. If the stuck pane is the orchestrator’s own, the notice rides back on its next tool call instead of being typed into it.
When you fix a stuck prompt yourself. A ⚠ stuck prompt chip means a prompt was typed into that pane but never submitted, and orrerix queues a repair — one Enter, held back until the pane is safe to write to. If you get there first (click into the pane and press Enter, or clear the box), orrerix now checks the pane rather than the keyboard: once the prompt is no longer sitting in the box, the repair is dropped instead of pressed a second time, the chip comes down, and anything queued behind it — steering messages, worker reports — starts flowing again on the next poll. Before this, your fix was invisible to the queue, and continuing to type in that pane kept the repair pending indefinitely, silently holding everything behind it.
The repair is only ever dropped once orrerix can see that the prompt has left the box. While it is still sitting there, orrerix keeps waiting for a safe moment to press Enter — because the alternative is pasting the next message on top of it and sending the two merged into one. So a pane blocked by your own half-typed line, or by a dialog waiting for an answer, still holds its queue, and the chip is what tells you it needs you.
And on a pane nobody is delivering to any more. The check above rides a repair or a delivery, so it used to stop once orrerix gave up on the pane — after which a chip on a finished worker or a drained orchestrator could stay up until you restarted. It no longer does: for as long as a ⚠ stuck prompt chip is up, orrerix re-reads that pane every half minute and applies the same rule. Two things have to be true together for the chip to come down by itself — the prompt is no longer in the box, and you have typed in that pane since orrerix submitted it. Either alone is not enough, deliberately: a prompt that vanished with nobody at the keyboard is exactly the case where the chip is the only trace left of it, and a keystroke on its own says nothing about where the prompt went.
Between those, the wording can still change under you. A chip that said orrerix could not read the pane, or that its repair attempts were used up, becomes “its text is gone — check the pane” once orrerix can see the box is clear. That is the chip getting more honest, not a new problem: it stays up, and it stays up until you deal with it or dismiss it. Whatever takes a chip down — your ✕, or orrerix’s own reading — is written to the group’s audit log with what it was and what was read, so you can always look up where one went.
One chip is answered by orrerix retrying, not by orrerix looking. A ⚠ stuck prompt chip that says the repair could not even be queued — the pane already had the maximum number of messages waiting — is not a claim about what is in the box, so re-reading the pane could never settle it. Instead, once that pane’s queue has drained and nothing is delivering to it, orrerix queues the repair it could not queue before, and the chip changes to “loomux is re-sending it”. From there it behaves like any other repair: held back until the pane is safe to write to, dropped if you get there first, and gone once the message lands. If the queue is still full, or a delivery is in progress, the chip stays exactly as it was and orrerix tries again shortly — nothing is taken down on a guess. Before this the repair was simply never attempted again, so a pane that filled up once could carry that chip until you restarted, with the prompt still sitting unsent in its box.
Lock resources (taking turns on something scarce)
Some things on your machine only one agent can use at a time — a compile that saturates every core, a GPU, a device on a USB port, a fixed port number, a staging database. Without help, four workers will reach for it at once. Lock resources let you declare those things once, in your repo, and have agents take turns.
Declare them in .orrerix/workflow.yml (the same file that carries your roster, so this needs the advanced orchestrator on):
resources:
build:
slots: 1 # how many agents may hold it at once — 1 is a mutex
max_hold_minutes: 45 # orrerix takes it back after this, whatever happens
gpu:
slots: 2 # omitting max_hold_minutes means the default, 30
What a resource means is entirely yours — orrerix never learns that build is a compiler. It knows the name, the slot count and the clock. Declare nothing and the feature is off: the agents in that group are not even offered the tools.
Both settings are optional, and the defaults are not what the example above shows. Omit slots and you get 1; omit max_hold_minutes and you get 30, not the 45 written out in the example. A name may use letters, digits, - and _ (up to 48 characters) — anything else is rejected rather than quietly rewritten, so the name you type is the name your agents call.
A value out of range fails the whole file, on purpose. slots: 0 or above 64, max_hold_minutes: 0 or above 480, a name with an illegal character, an unrecognized key inside a resource, or more than 32 resources are all hard errors that stop .orrerix/workflow.yml from loading at all — taking your roster and merge gate down with them, and the launcher will show you why. That is deliberate: a repo that wrote slots: 0 believes its builds are serialized, and quietly substituting a default would leave that belief in place while the behaviour changed underneath it. If your workflow file suddenly stops loading after you add a resources: block, this is the first thing to check.
What the agents get. Three tools, and only in a group whose repo declares resources: acquire_lock(name, note?, wait_minutes?), release_lock(name), and list_locks(). The names you declared are listed in the tool’s own description, so an agent can see what exists rather than guessing. Your orchestrator is told to name the relevant lock in a brief; a worker’s instructions tell it to take the lock before the work and release it as soon as that work is done.
Nothing blocks. acquire_lock answers immediately — either the lock is yours, or you are queued at a stated position. A queued agent ends its turn and gets a [orrerix] lock 'build' is yours notice typed into its pane when its turn comes, exactly like a CI watch resolving. That is not a convenience: a notice is typed into a pane, and a pane that sat blocking on its own lock could never receive the one telling it to proceed.
The queue is first-come, first-served, and every wait is bounded: a queued request gives up after wait_minutes (default 60, 5–240) and the agent is told so, rather than waiting on a notice that will never arrive. An agent that changes its mind can call release_lock while queued to withdraw, so nobody sits behind a slot it no longer wants.
A lock always comes back. Three things can end a hold: the holder releases it, the holder’s pane dies (orrerix reclaims it immediately and hands it to whoever is next), or the hold runs past max_hold_minutes (orrerix reclaims it and tells the ex-holder its work is no longer serialized, so it can re-acquire). There is no state in which a resource is stuck forever because an agent forgot.
Pausing a group freezes the clocks: nothing expires, nothing times out, and the paused span is credited back afterwards, so a long pause never costs a running build its lock. It does not freeze you — killing a holder’s pane while the group is paused still reclaims that lock and still hands it to whoever is next, because that is your deliberate action rather than a timer firing. (The notice telling the new holder sits in its pane’s queue until you resume, like every other delivery to a paused group.)
Where you see it. The group lifecycle panel (Alt+O) grows a lock section: one line per declared resource with how many slots are taken, who holds each (with the note it gave, e.g. w-3 (cargo test) 12m) and how many agents are queued behind it. Hover for the full queue in order, with each agent’s own clock. The line turns amber when somebody is waiting and red when a reclaim is imminent. The audit viewer (Alt+A) has a sentence for each lifecycle event — taken, queued, released, granted, reclaimed, timed out — so “why did that build take 40 minutes” is answerable after the fact.
Lock state lives in memory only. Closing orrerix clears it, which is the right answer: every pane that could have been holding a lock died with it.
It is cooperative, and deliberately so. A lock is taken because an agent asked for one — orrerix does not intercept your build command, and an agent that never calls acquire_lock is not stopped. An earlier design tried to enforce this by shadowing the guarded program on PATH, and it was abandoned: a shim only catches the shells it shadows, so the guarantee it appeared to give was not one it could keep. Advisory locking that is honest about being advisory, with a full audit trail of who held what and for how long, is worth more than enforcement with a hole in it.
Cross-workspace channels
Every orchestration group is isolated by design — one group’s agents never see another’s context. Sometimes you want a narrow, explicit exception: two related repos open in different tabs (a library and its consumer, a backend and its frontend), and you want one agent to tell another “the API changed” or “I’m blocked on your PR” without relaying the message through you. A channel is that exception, and it is opt-in every time: only you can open, close, or redirect one. No agent can ever connect, join, disconnect, or redirect a channel itself.
(The pane header’s right-click menu carries one other gesture, below the channel items: Promote to orchestrator… on a standalone Claude pane — see Promoting a standalone agent. Promoting a pane closes any channel it was in, since the promotion retires the standalone identity that channel was keyed to.)
Connecting. Right-click an orchestrator, worker, reviewer, or standalone agent pane’s header and choose Connect… — the pane arms (its header outlines with a pulsing dashed border) and waits for its peer. Right-click a second pane, in the same tab or a different one; the completion menu asks you to pick a direction — A → sends to → B or B → sends to → A — an explicit arrow, chosen at the moment of connecting, not guessed from which pane you armed first. Right-click the armed pane again, or press Esc, to cancel before completing it. Once connected, both panes show a small colored, numbered chip (⇄1, ⇄2, …) before their title, plus a direction arrow (▲ for the sender, ▼ for a receiver) — the color and number are the SAME on every member of one channel, so with several channels active at once you can tell at a glance which panes belong together, and who’s driving each one. The number reflects what’s currently connected, not a running count: if ⇄1 disconnects, the next channel you connect gets ⇄1 again rather than counting up forever — the number always matches how many channels are actually active right now. The chip mirrors to a docked pane’s minimized chip too, and a background tab holding a connected pane gets its own small dot on the tab strip, so a channel spanning a hidden tab is never invisible.
Sender and receiver. A channel is directional: one member is the sender, everyone else is a receiver. The sender’s channel_send(text) broadcasts to every receiver, any time — it lands as a typed [orrerix] channel chan-N - <name> (<role>, <repo>): <text> message in each peer’s own pane, the same visible-prompt delivery every other agent-to-pane message already uses. A receiver’s channel_send is reply-only: it works once the sender has messaged that receiver (one reply per message, to the sender only — never to another receiver), so two agents can’t talk over each other. channel_status() tells an agent who it’s connected to, who’s driving, and whether it can send right now. Both tools are denied to planners (like the CI-watch tools above) since a planner’s pane closes the instant it reports done.
You can hand the sender role to a different member without reconnecting: right-click a connected pane and choose Make this pane the sender (only offered on a receiver that can actually hold the role — see “receive-only”, below). The swap clears every pending reply credit, so both sides start clean under the new direction.
Standalone panes. A plain Agent pane (opened outside an orchestration group) can join a channel too, not just orchestrator/worker/reviewer panes. Launching a fresh claude or copilot agent pane wires it up automatically — nothing to do, it just shows up as a normal Connect target — as long as the launcher’s Channel tools checkbox is on (it defaults on; turn it off if you’d rather a fresh pane not carry a live channel token until you actually connect it — the checkbox only appears for claude/copilot, since it’s the only pair this applies to). Any other CLI (codex, gemini, opencode, a custom command), a claude/copilot pane launched with the checkbox off, or any pane that was already running before its channel tools were wired up, becomes connectable the first time you right-click it: it joins as a receive-only member (a dashed variant of the chip, instead of solid) — it can never be the sender, and its direction is always ▼. This is a structural fact, not a bug: those CLIs have no way for orrerix to hand them a channel-send capability today (tracked as a follow-up), and an already-running pane can’t be handed one either without restarting it. A receive-only pane still gets every message the sender sends it — it just can’t talk back.
Multi-party. A channel can have more than two members: right-click a free (not yet connected) THIRD pane’s Connect…, then right-click an already-connected pane’s completion menu — since that channel already has a sender, the only option is Join as receiver — driven by <sender>, so a newcomer can never accidentally become a second sender. A pane can only ever belong to one channel at a time; connecting an already-connected pane to a different channel is rejected (disconnect it first) — that limit is what keeps the chip unambiguous and keeps two channels from silently bridging through a shared pane.
Disconnecting. Two equally-easy ways: the pane’s context menu Disconnect item, or a single click on the channel chip itself. Either removes just that one pane. If that drops the channel below two members — or if the pane you disconnected was the sender — the whole channel closes and every remaining pane is notified: a channel with no one driving it is as dead as one with only a single member left, and there’s no automatic promotion (a human always picks who sends).
Limits. Channels are in-memory only — closing orrerix drops every channel; after a restart, reconnect the panes you want linked. A pane holds at most one channel at a time (see Multi-party, above). Full (sender-capable) standalone membership only works for claude/copilot today — see “Standalone panes” above.
Forking an agent’s session
A fork opens a new agent whose conversation starts as a copy of an existing agent’s, while the original keeps running untouched — for a side quest that needs what that agent already knows (an alternative approach, an investigation off a half-finished task), without briefing a fresh agent from nothing. It is the CLI’s own fork, never a copy of its files: Claude Code, codex, pi and opencode can fork; copilot and gemini have no command-line fork, and orrerix refuses them with the reason.
Who can fork what. The orchestrator calls fork_session(agent, task?, worktree?, branch?, name?); so does a lead, for one of its helpers. You can do the same from any worker, reviewer or planner pane’s header menu (Fork session…), which asks for the fork’s name first (Session browser). A fork of a delegate is a new delegate of the same block — persona, CLI, model and capability class — and it:
- counts against the group’s agent limit and the spawn-rate limit, like any spawn;
- gets its own worktree when the original is a worker or reviewer, cut from the original’s branch (so it starts at the original’s last commit — uncommitted work in the original’s worktree is not carried); a planner fork, like any planner, gets none;
- is told on its first turn that it is a fork, of which agent, and what its task is — or, with no task, that it should wait for one rather than carry on with the original’s work;
- reports to the orchestrator (or the lead) like any delegate, under its own agent id.
What is refused, each with its reason: an orchestrator, manager or lead as the source; a pane a live review or plan drive is using (the drive briefs and routes its panes by agent id, and never briefed the fork); a CLI with no fork; a block on the structured driver; and an agent whose session orrerix has not recorded yet (codex and opencode learn theirs a few seconds after the first prompt).
A lead forking its own pane gets a standalone pane beside it, for you — never a second lead. It does not report to the lead; it is yours, and it opens under the name the lead passed (or <lead pane name> (fork)). The audit log records the lead’s request (agent-fork-requested, with that name) and then what happened to it: agent-fork once the pane opened, or agent-fork-failed with the reason (for example, the lead’s pane is not open in this window) — which is also shown to you.
What you can see. A fork of a worker, reviewer or planner writes one agent-fork row in the group’s audit log, naming both sides (parent_agent, parent_session, the new agent and — where the CLI names it up front — its session) and who asked. That row is written when the fork is admitted, beside the new agent’s ordinary agent-spawn row and before its pane has opened — so it records that the fork was started, not that its pane came up; a fork whose pane then fails to open shows up the way any spawn’s does, as that agent’s agent-exit. (A lead’s fork of its own pane is the other way round, above: a request row first, then the outcome.) The group’s agents.json records the parent session on the forked agent’s row (forked_from).
No merge-back. Nothing joins two conversations again — no CLI offers it — so a fork’s results come back the way any delegate’s do: its report, and its own branch and PR.
Group lifecycle
The orchestrator pane has a lifecycle toggle (Alt+O or the group icon) with a one-glance summary — how many agents are live, the role breakdown, uptime, each agent’s state, and running session cost with a group total.
Where those cost figures come from. Tokens are exact — read from each CLI’s own record of the session, never scraped off the pane. Dollars depend on the CLI. For Claude Code, orrerix prices the tokens itself against a dated table, so the figure is an estimate — and on a subscription/Max account the real marginal cost is $0 regardless, which is why tokens are the honest metric there. OpenCode and pi price their own sessions, so orrerix reports their numbers instead of guessing one (including a genuine $0.00 on a free model, which is an answer, not a blank). Each total is labelled accordingly — estimated, reported, or mixed for a group running both kinds. Codex records tokens and no dollars at all, and orrerix’s price table has no Codex model in it, so a Codex agent shows exact tokens and a blank dollar figure rather than an invented one. A CLI with no readable record falls back to whatever dollar figure it prints in its own statusline, which disappears when the pane does.
Those figures are totals, and orrerix now also keeps a history. While a group is open, orrerix records each agent’s running token counts into a small per-group file roughly every five minutes, and only when something actually moved — so an idle agent writes nothing and the file stays small. That is what a spend-over-time chart will read. Two things worth knowing about it: it is never rebuilt, so history starts the first time a build that records it ran against that group, and every panel built on it says how far back it goes rather than drawing a flat line where there is simply no data. And it notes the moments the fleet’s own configuration changed — the workflow file, the personas, the skills, CLAUDE.md, the lessons file — so a change in spend can be lined up against the change that might explain it.
Two things that make Codex rows look empty, and neither is a fault. A Codex pane has no usage at all until orrerix has identified which session it is running — Codex offers no way to tell it a session id up front, so orrerix watches its store and binds the one the pane creates, which happens when the pane does its first real work. And Codex compresses a session’s transcript about a week after its last write; orrerix does not decompress, so a session that old reports no usage. Live panes are never affected. What you can lose is the lifetime figure for an old session that was never snapshotted when its agent exited.
When the panel says stale. The lifecycle panel and the tab strip’s agent counts are served from a snapshot orrerix refreshes about once a second, rather than by asking the backend a separate question per figure every time — ten of them for the panel, and two per tab for the strip. If that refresh falls behind — the usual cause is one long-running internal operation holding things up — the panel keeps showing you the last figures it has and puts an amber stale 12s badge in its header saying how old they are; the affected tab counts go italic and their tooltip says the same. Nothing is lost and there is nothing to click: the badge disappears by itself the moment a fresh snapshot lands. It is deliberately never cleared by a timer, so a badge that is still up means the figures really are still old. Numbers shown while it is up are true, just from a moment ago — which is why orrerix shows them rather than blanking the panel.
When it says partly stale. Same badge, narrower claim: most of the panel refreshed and one part of it could not. The figures you are looking at are still true, and the age on the badge is the age of the OLDEST part — so a partly stale 40s panel is telling you that one section is 40 seconds behind, not that everything is. It clears itself the same way.
When an agent reports loomux busy. Agents talk to orrerix over a small local server, and if some internal operation is holding things up, a call that would otherwise have waited indefinitely is answered instead with a message beginning loomux busy: — naming what is held, for how long, and by which part of orrerix. This is a normal, retryable answer, not an error to report: the agent is told nothing was executed and it can simply try again. Two things are worth knowing if you see one:
- A busy answer never means a half-done change. Only READS are answered this way. Anything that changes something is left to run — if one of those takes a long time the agent is told it is still executing and explicitly told NOT to re-issue it, because a slow change finishes on its own. The one way a change does not finish is a bug inside orrerix, and that is never reported as busy: it gets the
internal erroranswer below, which says so and tells the agent to check. - The breadcrumb log names the culprit. Each one is recorded once, with the holder and the duration, in
logs/breadcrumbs.logunder your orrerix data directory. If busy answers keep coming, that file is what to send.
When an agent reports internal error: … ended without a result. This one is rare and it is not the same thing as a busy answer: it means the change the agent asked for hit a bug inside orrerix and stopped partway rather than finishing. The agent is told which read tool to check with — list_tasks, list_agents and so on — and it should look before trying again, because the change may have been partly applied. A crash log naming the fault is written into logs/ under your orrerix data directory at the same moment; that file, with logs/breadcrumbs.log, is what to send. Nothing else stops: the rest of orrerix keeps answering, and the agent can carry on with the next thing.
When a panel says orrerix “refused that to avoid deadlocking itself”. The same class of internal bug, reached from a button rather than from an agent. The action you clicked did not run, it may have partly applied, and the rest of the app keeps working — so check whatever you were changing before clicking again. This one writes no crash log (nothing crashed; orrerix stopped itself on purpose), so logs/breadcrumbs.log is the file to send. It should never appear; if it does, that is worth reporting.
From the lifecycle panel you can:
- Pause the group — orrerix stops delivering prompts so its agents finish their turn and idle out (reversible with resume). Pausing holds deliveries rather than dropping them: nothing is typed into a pane while you’re paused, so nobody spends tokens, but a worker’s
donereport fired mid-pause is queued — on disk, so it survives a restart taken during the pause — and delivered when you resume, labelled as having waited on the pause rather than on a blocked pane. An agent spawned during a pause is held the same way, and resumes as the boot it is: orrerix still waits for its CLI to finish painting and still answers Copilot’s “Enable autopilot mode” dialog before typing the brief, instead of pasting into a half-booted pane and leaving the agent sitting at a consent dialog nobody dismissed. Two things still refuse rather than wait, because both are you acting now: the compose strip’s Send and a task’s Start button both tell you to resume first instead of deferring your message to a moment you haven’t picked. And the hold is not unlimited — each pane queues at most 8 deliveries, so a very long pause on a busy pane can start refusing new ones; the sender is told, and on resume the orchestrator is told what was refused. - End orchestration — kills every agent in the group at once (two-click confirm; it’s destructive). An optional remove worktrees checkbox also deletes each agent’s git worktree — uncommitted changes are lost, but the branches (where the PRs live) are always kept.
- Max live agents stepper (1–12) — adjust the cap on the fly; orrerix persists it, audits the change, and tells the orchestrator to re-plan against the new ceiling. Lowering the cap below the current live count never kills anyone — it just blocks new spawns until attrition brings the count back under.
- Fold panes — the same group-wide minimize/restore as the orchestrator header, for reclaiming screen space.
- Workflow row — when the repo has a custom agent workflow active, this panel names it, lists its roster, and shows the armed merge gate in one line (e.g. “loomux · 9 blocks · merges to the default branch require: rev-orch + rev-ui + rev-tests · all-pass · ci-green”) — so you know whether an Approve can actually succeed before you click it, not after it bounces. If the gate names reviewers the current roster can’t spawn, the row warns loudly instead.
- Advanced-orchestrator toggle — flip a repo’s custom workflow on or off live, no relaunch: the merge gate and the roster for future spawns update immediately, and the orchestrator’s pane gets an
[orrerix] workflow mode changed: …notice so it can adjust its spawn/review strategy mid-session. Agents already running keep the role they were spawned under; only new spawns pick up the swapped roster. - Workflow picker — when the repo declares more than one workflow, pick another and Review & apply it without relaunching: you get a diff of what would change first, and nothing applies until you confirm it. A drift chip appears beside it when the file the group was launched from has been edited since — the group keeps running the roster it pinned, and applying the same workflow again is how you adopt the edit. Edit… opens the selected workflow in the visual designer. The picker is greyed out while the advanced-orchestrator toggle above is off: that toggle is the consent for running a repo-authored roster at all, and the picker only chooses which file. See switching a running group’s workflow.
Custom agent workflows
By default a group runs the built-in four-role roster — one orchestrator, worker, reviewer, and planner, each on the CLI/model you picked at launch. A repo can commit <repo>/.orrerix/workflow.yml and declare its own instead: any number of named blocks, each with its own capability class (orchestrator, worker, reviewer, planner, or manager — the last being the pane you talk to, see below), CLI, model, and persona, plus a merge gate naming which reviewer blocks must record a pass verdict — enforced mechanically by the gh shim — before gh pr merge can succeed. See docs/design/workflows.md for the full design.
More than one workflow per repo. A repo can declare several. .orrerix/workflow.yml is the workflow named default; every <name>.yml under .orrerix/workflows/ is a workflow named <name>, in the same format:
.orrerix/
workflow.yml # the workflow named "default"
workflows/
review-heavy.yml # the workflow named "review-heavy"
solo-fast.yml # the workflow named "solo-fast"
Picking one at launch. When a repo declares more than one workflow, the setup pane grows a Workflow dropdown under the Advanced orchestrator checkbox, listing every workflow the repo declares. Pick one and the roster box beneath repaints to that workflow’s blocks — the same resolved roster the group will run, so what you consent to is what launches. The dropdown is not shown for a repo with one workflow or none: there would be nothing to pick, and the form is the one it has always been.
The dropdown appears only while Advanced orchestrator is ticked, because the two are different questions. The checkbox is the consent — whether a repo-authored roster runs at all — and the dropdown answers only which file. Untick the checkbox and your choice is still remembered with the group, so turning workflow mode on later comes back to the workflow you picked rather than to default.
Editing them. Edit workflow… opens whichever workflow the dropdown is showing, so a repo with several is edited one file at a time in the visual designer, each with its own canvas. Node positions are saved beside each file — workflows/review-heavy.layout.json next to workflows/review-heavy.yml — so arranging one workflow’s blocks never disturbs another’s. As before, the button turns this setup pane into the designer, so open a new pane to launch once the file is right.
A name may use letters, digits, - and _ only, and it is what the file is called — review-heavy.yml is the workflow review-heavy. Anything else (a dot in the name, a path separator, a leading -, a Windows device name like CON) is refused rather than quietly corrected: the file is simply not a workflow, and orrerix records why alongside the listing rather than dropping it without a word. A workflow whose file has a syntax error stays in the dropdown, marked (has errors) — it is the one you need to open in order to fix it, and the roster box shows you every finding. .loomux/workflows/ is read when .orrerix/workflows/ is absent, the same way .loomux/workflow.yml is.
The workflow a group runs is recorded with the group, next to the roster it produced, so a resumed orchestration comes back on the same file. A repo that has only .orrerix/workflow.yml is unaffected in every respect: it has one workflow, it is called default, and nothing under workflows/ is ever opened. If you have both a workflow.yml and a workflows/default.yml, the plain workflow.yml is the one that is read — rename the other rather than leaving two files claiming one name.
Moving between workflows inside the designer
Once the workflow pane is open you do not have to leave it to work on another of the repo’s workflows. The file name in the pane’s header is a button. Click it for the list of every workflow the repo declares — the same listing the launch-time dropdown and the group header read — with a tick beside the one you are editing, and New workflow… at the bottom.
Choosing another one opens it in the same pane: its own canvas, its own node positions from its own layout file, its own findings. The pane renames itself after the file (unless you have renamed the pane yourself), and the file it is on is remembered with your layout, so a restored pane comes back on the workflow you were last editing rather than on default.
A workflow whose file will not parse is in this list too, marked (has errors) with the first finding as its tooltip — it is the file you opened the designer to repair, so it is never hidden. If the pane is on some other .yml (the file browser’s Open in workflow pane takes any of them), the list says so at the top rather than ticking a workflow you are not looking at.
Unsaved edits are never carried across. Switch with edits outstanding and the pane asks first, with three answers: cancel, save them into the file you are leaving and then switch, or discard them. Nothing is written to the file you are switching to — the edits belong to the file they were typed against, and the one thing the pane will not do is quietly move them.
New workflow… asks for a name, creates workflows/<name>.yml scaffolded from orrerix’s built-in roster, and opens it. The name is checked as you type, against the same rule the rest of orrerix uses: letters, digits, _ and -, not default (that name belongs to the repo’s own workflow.yml), and not a name already taken. It is also refused when it differs from an existing workflow only by capitalisation — Review beside review — because on Windows and macOS those are one file, and a repo carrying both has a workflow whose identity depends on which machine reads it.
Switching a running group to another workflow
A group does not have to be relaunched to change workflow. The lifecycle panel (Alt+O) has a workflow picker beside its workflow row: choose another of the repo’s workflows and click Review & apply.
You get a diff before anything happens — which blocks the switch would add and remove, which ones change and in what (cli, model, prompt, …), the merge gate it would arm, and whether the switch changes the intake label vocabulary your own hold veto uses. Nothing applies until you confirm that diff. If the file is edited between the diff and your click, the apply is refused and asks you to re-open it, so what lands is what you read.
What a switch does not do is re-personae anybody. Agents already running keep the block they were spawned under — a switch changes the roster future spawns resolve against, not the panes in front of you. A pane whose block the new workflow drops keeps running; what is refused afterwards is a bare resume of its session, because the group can no longer spawn that block.
Two things a switch will refuse outright, and it tells you which:
- A change to the orchestrator block’s CLI. That pane is already running a program and cannot change it mid-session. You still see the full diff and the reason — end the group and relaunch on the new workflow if that is the change you want.
- A switch on a group whose Advanced orchestrator toggle is off. The toggle is the consent for running a repo-authored roster at all; the picker only answers which file. Turn workflow mode on — which arms the workflow the group already has pinned — and then apply.
Drift. If the workflow file is edited on disk after the group launched, the group keeps running the roster it pinned. That is deliberate: a file appearing or changing never re-rosters a live group behind your back. A drift chip appears in the panel saying so, and applying the same workflow again is how you adopt the edit — the same confirmed action, with the edit as its diff. The chip also appears when the file has been deleted or has stopped validating, and in that case the picker still lists the workflow the group is running, marked as having no file, rather than quietly showing you a different one — and Edit… tells you the file is gone rather than opening a different workflow under its name. To bring it back, write the file itself — workflows/<name>.yml under the config directory this repo uses, in any editor. Once it exists, both pickers list it again, the file browser’s Open in workflow pane opens it in the designer, and Edit… works from the group panel. Don’t reach for the setup pane’s Edit workflow… for this: its dropdown is built from the same listing, so a workflow whose file is gone can’t be selected there either, and the button would open the repo’s default workflow instead.
The orchestrator’s read-back (list_blocks). Switching a running group to another workflow is confirmed by the human before anything applies; when a switch lands, the orchestrator’s pane gets a one-line [orrerix] workflow switched: <old> → <new> notice naming the new roster. Its kickoff’s workflow section tells it what that notice means — spawn by the new ids, and read removed: as ids it can no longer spawn or bare-resume. Because a notice can be compacted away, that section also names the durable read-back: the list_blocks tool returns the active workflow’s name and every block’s id, capability class, CLI, model and persona — the same roster its spawns resolve against, whatever its kickoff quoted at launch. The paragraph naming it is workflow-conditional: a group running the built-in roster reads nothing about a switch in its kickoff. The list_blocks tool itself is not gated — it is listed for every orchestrator, and it answers a built-in group honestly (workflow: "default" with the built-in roster), because the listing machinery never sees the roster and a caller that asks deserves the truth rather than a refusal.
On .orrerix/ and .loomux/. The config dir used to be .loomux/, and a repo that still has one keeps working with no action: each file — workflow.yml, lessons.md, workflow.layout.json — is looked for in .orrerix/ first and in .loomux/ if it is not there. Orrerix never renames that directory for you; it is committed to your repository, on your branches, and moving it is a commit only you should make. If both exist, .orrerix/ wins, so you can migrate one file at a time and see each move take effect immediately.
If your repo squash-merges, consider also: [body-unchanged]. A verdict is bound to the commit it reviewed, so a re-push re-opens the gate. The PR body is not part of that commit — and a squash merge turns it into the permanent commit message, so a body edited after a reviewer passed lands text nobody reviewed. orrerix always records a digest of the body a verdict reviewed and tells the orchestrator when it has moved (on a pass: the approval no longer covers what would be committed; on a fail: the finding may already be fixed). Adding body-unchanged to your gate’s also: list also refuses the merge until the reviewers whose passes are live have re-recorded against the body as it stands. It is opt-in because it is only true of squash-merging repos; where merges keep the PR body as discussion rather than history, leave it out. On a gate that declares the condition, the gate line the orchestrator reads agrees with the refusal: while a pass has drifted and no verification round has superseded it, the line reads NOT YET SATISFIED, names the lane, and says gh pr merge is refused — a body nobody approved is as blocking as a commit nobody reviewed, because the squash message is unamendable after merge. While the body-unchanged condition is failing — the one merge-time condition the gate line itself can see, since it never spends a gh call on the others — the gate line also withholds the “merge this PR from the GitHub UI” exit and offers the two ways that go through the gate instead; the exit comes back once the condition is met. (Conditions the shim checks against real gh at merge time, like ci-green, are invisible to the gate line, so the exit stays for them there; the shim’s own refusal always names all three.)
One case does not cost every reviewer a re-read. When the review driver is running a PR and the body changes after every required lane has already passed the code at that head, it re-briefs one lane — the first in your gate’s reviewers: list — and asks it for the body as it stands rather than for the diff again. That lane’s pass is recorded as a body verification, and the gate then accepts the other lanes’ passes: they are bound to the same commit, so the code they approved has not moved, and a reviewer you named has read the text that would be committed — so the gate line keeps saying SATISFIED, with a note that the drift was accepted rather than a warning to re-record. Everything else is unchanged — a fail still blocks, a lane that has recorded nothing still keeps the gate shut, a pass at an older commit is still stale, and a further body edit spends the verification and re-opens the question. Only the driver can produce one of these, so if you record verdicts by hand the clause behaves exactly as described above.
Keep the batches small: max_diff_lines. A merge gate can also declare a size limit — gates.merge: { max_diff_lines: 800 } — and orrerix refuses any merge of a PR that changes more than that many lines (additions + deletions, across the whole PR). The reason is the one thing this whole feature rests on: a review nobody can hold in their head is a review that rubber-stamps, and an oversized PR is the standard way an agent fleet defeats its own review gate. The number is yours; orrerix never invents one. Omit the key for no limit — 0 is refused rather than read as “unlimited”, because a bound that bounds nothing is a typo. You also get an advisory at gh pr create time, printed into the pane of the agent that opened the PR, so a split can happen before review effort is spent rather than after; that advisory is best-effort and never blocks or delays the PR being created. The refusal at merge time is the enforced half, and a PR whose size orrerix cannot read is refused too.
Stop the line: also: [base-green]. Nothing otherwise stops agents merging more work onto a branch whose HEAD is already broken — which compounds failures and makes the merge queue’s bisect unable to say which change was at fault. Adding base-green to your gate’s also: list refuses a merge while the HEAD of the PR’s base branch is red, still running, or reports no checks at all. Opt-in, and deliberately strict: “orrerix could not tell” is never treated as green, so a repo whose CI legitimately skips some commits (leaving a base commit with no checks on it) should not declare this — every merge onto such a commit would be refused until something ran there.
The same strictness covers a case you would otherwise never think about: GitHub returns a commit’s check runs one page at a time, so a base carrying more checks than a single page can report is treated as unreadable — not as green — and the merge is refused. Orrerix asks for the largest page the API allows (100), so this only bites a base with more than 100 checks on one commit; if that is permanently true of your default branch, base-green cannot be enforced for it and should not be declared.
Route reviewers by path: routing:. A gate’s reviewers: list is the same on every PR, which leaves a multi-lane workflow choosing between requiring every lane on every PR and leaving the decision to prose. routing: makes the required set depend on what the PR actually changed:
gates:
merge:
require: all-pass
reviewers: [rev-lead]
routing:
- paths: ["src/**"]
reviewers: [rev-ui]
- paths: ["**/Cargo.toml", "package-lock.json"]
reviewers: [rev-deps]
A PR that touches only docs needs rev-lead. One that touches src/ needs rev-lead and rev-ui. One that adds a dependency needs rev-deps as well — which is the “an agent quietly added a dependency” review you would otherwise be relying on someone to remember. Rules only ever add: the required set is your reviewers: list plus every rule that matched, so writing a rule can never make a gate easier to satisfy. A routed reviewer is treated exactly like a declared one from there on — its pass goes stale on a re-push, its fail blocks the merge, and the refusal names it and the rule that pulled it in.
The globs are orrerix’s own, not your repo’s CODEOWNERS (that file names GitHub users and teams; a gate names workflow blocks). They are deliberately simple — one wildcard and one special case:
| You write | It matches |
|---|---|
src/** | anything under src/, at any depth |
src/*.ts | also anything under src/ ending .ts, at any depth — * crosses / |
**/Cargo.toml | every Cargo.toml, including the one at the repo root |
package-lock.json | that exact path, and nothing else |
* matches any run of characters including /, a leading **/ is optional, and the match covers the whole path rather than part of it. That is coarser than .gitignore, on purpose: a glob that matches too much asks for one review you did not need, and a glob that matches too little skips one you did — so the simple rule is the one that errs the safe way. Write file globs, not directories: src/**, never src/ (and never a leading / or a .. segment) — those match nothing at all, so orrerix refuses them at load rather than letting a rule silently never fire.
Two things to know before you adopt it:
routing:andrequire: thresholdcannot both be declared. A threshold counts passes over a fixed list; routing makes the list depend on the diff, so together an extra lane could supply one of the required passes instead of adding one. Rather than guess which you meant, orrerix refuses the file. Userequire: all-passwithrouting:.- A PR whose changed files orrerix cannot list in full is refused. GitHub returns a PR’s file list one page at a time and orrerix can only ask for 100, so a PR changing more than 100 files cannot be routed — it is refused rather than treated as matching nothing, because “no rule fired” and “we could not look” must never be the same answer for a gate. Split such a PR (which
max_diff_linesabove probably wanted anyway).
The workflow pane shows your rules and preserves them across edits, but does not offer a control for adding one yet — edit .orrerix/workflow.yml directly.
Verdict notices are a pointer, on purpose. Recording a verdict also types a courtesy notice into the orchestrator’s pane, so it learns the review landed without polling for it. That notice carries the routing facts and nothing else — which reviewer block recorded which verdict on which PR, the gate’s current status, and the list_verdicts("<pr>") call that returns the summary:
[orrerix] rev-9 (rev-std) recorded verdict PASS on PR #12 — list_verdicts("12")
Pane text becomes that agent’s resident context and is re-sent on every turn it takes afterwards, so a copy of every summary is paid for repeatedly — and the summary is not what the orchestrator decides on. Nothing is lost: the whole summary stays in the verdict record (the orchestrator reads it with list_verdicts, which is also what the merge gate reads) and in the review posted on the PR itself. Under a review drive you will not see this notice at all — the driver takes delivery of its own lanes’ verdicts and reports them in its own notice instead.
A reviewer declares how many findings it left open. review_verdict takes an optional open_findings — the findings the reviewer left open at that head, blocking and non-blocking together, 0 when nothing is left. A reviewer the gate names is asked for it on every verdict (in its block note, beside the rest of the verdict contract), and list_verdicts shows it on the verdict’s row. A verdict without it declared nothing, and orrerix never reads that as zero; a value that is not a whole number of at least 0 is refused, and the reviewer records again.
Opt-in, every time. A workflow file arrives with a git clone — the advanced orchestrator toggle is what makes a repo’s workflow take effect; off (the default), the file is never even opened. Turning it on, at launch or live (see Group lifecycle, above), shows you the resolved roster — every block, its CLI/model, and which ones carry a repo-authored persona — before anything spawns.
The gate belongs to the session, not to a PR’s history. Toggling the workflow off ungates every merge from that session onward, including a PR opened earlier while the workflow was on — a gate that outlived the toggle that armed it would be exactly the kind of surprise this feature exists to prevent. A human Approve grant still never opens the workflow gate by itself (see The task board, above); toggling the workflow off is what actually clears it.
orrerix never silently arms a gate it can’t satisfy. If a workflow’s merge gate names reviewer blocks the currently-running roster can’t actually spawn — most commonly a broken or missing workflow.yml on a relaunch — orrerix doesn’t drop the gate to keep merges flowing; it arms the gate anyway and shows a loud warning in the lifecycle panel, so the mismatch is something you see rather than something a bounced merge makes you go find.
Copilot personas: a tools: list must let orrerix through. If a block sets cli: copilot and points profile: at a .github/agents/*.md file, Copilot loads that file directly — and if the file declares a tools: list, that list filters everything the agent can reach, MCP servers included. A list that never names loomux produces a delegate that can see the loomux server and use none of it: it cannot report, read the task board, or be steered, and from inside its own pane it looks like orrerix is broken. Add the server to the list:
tools: ["read", "edit", "execute", "orrerix/*"]
orrerix repairs this for you where it safely can — it launches such a block from its own stand-in for the persona, carrying every line of your file plus that grant, and never edits anything in your repo — but it always tells you, in the spawn reply and the audit log, which file to fix.
Three cases it deliberately leaves alone, because each is a choice you made rather than something you forgot. It says so, and the fix is yours:
- the persona declares its own
mcp-servers:— orrerix’s stand-in would drop them; tools: [], which disables every tool — orrerix won’t turn “nothing” into “nothing except loomux”;- the list already names loomux per-tool (
loomux/report) — orrerix won’t widen a scope you set on purpose.
Picking a block’s model. The workflow pane’s block form offers the same model dropdown the launcher does, filled the same way: the CLI’s own reported models first, backed by orrerix’s suggestions, plus a custom… entry for any id neither list carries (a Bedrock inference profile, a gateway deployment name, a model newer than your build). A CLI orrerix has no suggestions for and can’t get a list out of gives you that free-text box directly. Leaving the model unset is a real choice with its own row: the block then runs whatever orrerix defaults to for its kind on its CLI — sonnet/opus on Claude Code, auto on Copilot, pro on Gemini, and on OpenCode no --model at all, so your own config decides.
Almost every setting in the file is editable in the pane. Beside the roster’s block rows and its merge-gate row sit four more: Intake, Merge queue, Review driver and Resources - the same intake:, merge_queue:, driver: and resources: blocks described elsewhere on this page. Each has an enable-toggle, and what the checkbox reads differs by what the section’s enabled state is. For Intake and Resources the checkbox is the section itself: switching it on writes the block, switching it off removes it. The Merge queue’s checkbox is its presence too, with a separate dropdown for the enabled: line - that file has three states and a checkbox has two. The Review driver’s checkbox is the enabled: line itself: ticking it on writes enabled: true (keeping any counters the block already declares), and unticking keeps what the block carries: a block that holds nothing but the switch is removed whole — absent and off are the same thing to orrerix, and deleting is tidier — while one carrying counters or comments has enabled: false written into it and loses nothing. A block that declares counters but no enabled: line shows unchecked, because that is what the engine reads as off. When that line carries its own trailing comment and a flip there would rewrite the value in place and change it — the line’s value ends in a true/false spelling and is not already the value the flip writes — the form says so beside the toggle: flipping the switch rewrites the value on the line and leaves the comment exactly as written — orrerix never edits your prose, and does not guess whether a comment still agrees with the switch, so the note is the cue to read the line after a flip. (A value that ends in a true/false spelling but is spelled with uppercase letters — True, FALSE — is one the pane flags, since its reader is lowercase-only where the engine is not; a flip there rewrites the value to lowercase and keeps the comment.) The driver’s counters and timeouts are number fields clamped to their own declared ranges, so the form cannot write an out-of-range value at all; what the engine then does with a hand-written value outside those ranges differs by field, and the next paragraph says which. The block form covers the rest: role_hint, and allow: as a list of tool patterns (one row per pattern, because a real pattern contains commas).
Two keys have no control. authored_with: deliberately never will — it records which orrerix created the file, is stamped once, and a save must never invent or restamp it. board: (WIP limits) does not have one yet: the pane reads it, keeps it, and writes it back untouched when you edit anything else, so editing your workflow in the pane is safe — you just set the limits in the text editor for now.
Two things those forms will not let you do, because orrerix’s engine would refuse the file: write a number outside a refused field’s range, and pair a role_hint with a kind that hint does not apply to. Which bounds the engine refuses and which it clamps is a per-family fact, stated here family by family — read each row as its own policy, never inferred from a neighbour:
- Merge gate (
gate:):thresholdis 1 or more, refused below that — and athresholdgate is also refused above its own reviewer count, a validation rule rather than a range.max_diff_linesis 1 or more, refused at 0, with the fix named in the error: omit the key to declare no limit. Neither has a ceiling. Both have form fields whose inputs enforce the bound — they cannot emit an out-of-range value — while a hand-written out-of-range value reaches you as a finding. - Merge queue (
merge_queue:):max_batchis 1 or more, refused below, no ceiling;checks_timeout_minutesis 5–240 minutes, clamped. - Review driver (
driver:):max_review_roundsandmax_ci_attemptsare 1–3 rounds each,max_rebase_attemptsis 0–1 rebases,plan_review_minutesis 0–120 minutes,planner_timeout_minutesis 15–180 minutes andfix_nonblocking_roundsis 0–3 rounds, all refused outside;lane_timeout_minutesandfix_timeout_minutesare 5–240 minutes anddrive_timeout_minutesis 5–1440 minutes, all clamped. - Lock resources (
resources:):slotsis 1–64 andmax_hold_minutesis 1–480 minutes, both refused outside; at most 32 resources may be declared. These are the fields the inputs themselves enforce — they cannot emit an out-of-range value at all. - Board WIP caps (
board.wip:): each status cap is 1 or more, refused below — a cap of 0 is a stop, not a limit — with no ceiling, and an omitted status has no cap. The board has no form yet, so these reach you as findings.
A value a hand-edited file already carries is shown as a finding instead, with the distinction that matters spelled out — a bound orrerix refuses reads as an error, one it clamps as a warning.
An untouched section is never rewritten. orrerix writes only what the file declares, so opening these forms to look at them changes nothing. This paragraph is about the single flip: what one click on the toggle does to the lines around the value it changes — not the on-then-off round trip, which is a different question with a different answer, and the two readings are kept apart below. For the toggle itself: unticking keeps what the block carries — a block that holds nothing but the switch is removed whole, while one carrying counters or comments has enabled: false written into it and loses nothing — and ticking on after unticking writes enabled: true and leaves the block’s lines byte for byte. A driver: block that carried no enabled: line at all is the case that starts differently: turning a driver on has to add that line, so the two clicks leave such a block declaring enabled: false where the file had none.
For one flip on an existing block: the lines around the value survive the flip byte for byte when the flip is an in-place value replacement on a block whose fields are on their own lines and whose value is spelled exactly true or false and is not already the target. What follows from it: a value that does not end in a true/false spelling at all (enabled: yes) cannot be replaced in place, so a flip regenerates the whole section and the block’s comments do not survive it; a value that already reads as the value the flip would write (enabled: nottrue under a checkbox that would write true) makes the flip a byte-identical no-op; a value spelled with uppercase letters (True, FALSE) is replaced but not with itself — the flip writes the lowercase spelling, so the line changes, the pane flagged the spelling, and the comment survives; and a block written in flow style (driver: {enabled: true, ...}) has no field lines to replace on, so a flip rewrites it in block style - the model is preserved, the prose is not. The note stays silent wherever the promise would not hold, and the pane flags every value the pane’s own reader cannot read (the lowercase-only list above), which is every one of these shapes on the file’s own terms. The round trip — the toggle on and then off, the gesture the pane actually offers — follows from the same rule rather than needing its own: where each flip preserves the lines around its value, the second flip is the first one in reverse, so on-then-off restores the file byte for byte; where a flip regenerates or rewrites the section, the file that comes back is not the one that left, and the shapes above are the ones where that happens. Unticking never deletes configuration either: a block carrying counters or comments is kept, with the switch written as off, and its comments are kept byte for byte except the switch’s own line.
Removing a section is a different gesture from the toggle, and the driver block is the one that has it. A red button at the foot of the driver form deletes the driver: block whole — the switch, every counter, and the block’s comments — behind its own confirmation that names what is discarded. This is deliberate destruction, not part of the toggle’s round-trip promise, and it exists because a driver: block makes the file unloadable on any orrerix build old enough to refuse the key (deny_unknown_fields on the workflow root, verified against v1.3.0-beta1): removing the block is how the file loads again. The toggle will not do this to you, and the button does nothing until you confirm it.
Reviewer diversity across models. A block’s cli/model are set per-block, so nothing stops a reviewer lane from running on a different CLI/model than the one that wrote the code — a second model tends to catch a different class of defect than the one already primed on its own output. Worth considering for any reviewer-heavy workflow; orrerix’s own dogfood .orrerix/workflow.yml notes the same above its reviewer blocks.
Cheap lanes ahead of the expensive one. A reviewer lane does not have to be a reviewer. Because cli and model are per-block, you can put a small, fast model on a lane whose whole job is running a fixed checklist of shell commands — is the evidence the PR body claims actually in it, does a cited CI run belong to the current head, does the diffstat match, is a forbidden import present — and name it in gates.merge.reviewers alongside your real reviewer. The strong lane then reads those results instead of re-deriving them, and spends its attention on the things no command can settle. Two rules make it work: write the cheap lane’s persona as a numbered checklist with the exact command on each line rather than as a description of what to look for, and tell it to fail only on the absence of something it can quote, escalating anything it cannot decide. A small model asked for judgment gives you unreliable judgment; asked what a command printed, it is accurate. Be clear-eyed about the trade: a cheap lane can pass a check it should have failed, so the strong lane stays in the gate and stays the bar — the cheap lanes buy its time, they do not carry its verdict. orrerix’s own repo keeps three such lanes as worked examples — qr-evidence.md, qr-tests.md and qr-constraints.md in .github/agents/, each a numbered checklist written exactly this way, and worth reading before you write your own. Its .orrerix/workflow.yml does not declare them today (its cheap tier is one iterating reviewer rather than fixed-checklist lanes), which is the other half of the trade: the shape is worth having in the drawer, not necessarily in every roster.
If what you want instead is an opinion on some PRs — a design review before code is written, a premortem on something risky — that is the opposite shape and wants kind: planner + role_hint: advisor, not a reviewer block. See Adding a second lens below. The question that separates them is not how valuable the opinion is, it is does this run on every PR? A checklist does, and belongs in the gate; a lens does not, and a reviewer block would both run it every time and hold every merge shut waiting for it.
Running a block on another machine: remote: (not usable yet)
A block can name an abstract label for the machine its agent should run on over SSH:
blocks:
- id: builder-remote
kind: worker
cli: claude # required, and spelled out — see below
remote: buildbox # a label, not an address
Right now this key does nothing. Orrerix parses it, validates it, and keeps it across a save — and then spawns the block locally, exactly as if you had not written it. It becomes real in two later steps: binding the label to an actual host (the operator side, #1458) and the remote spawn path itself (#1459). The key ships first, on its own, because it is the part everything else has to agree with.
The label is a name you choose, never an address. There is deliberately no host:, port:, user:, identity_file: or ssh-options key — writing one does not “not work”, it fails the whole file at load. workflow.yml is committed to your repository, so anyone who can open a pull request can edit it; a repo-authored hostname would let that person point execution at any machine you can reach. So the repo file only ever selects a name, and you — the operator, outside the repo — decide which host, which account and which clone path that name means. Same seam as personas: a repo picks from what you defined, and can never mint something new.
Two rules the parser enforces, both of which fail the file rather than warn:
- Not on an
orchestratorormanagerblock. Those two run where you are, and that is load-bearing: the orchestrator holds your orchestration state, itsghoperations and the merge gate, and a manager pane is the thing you type into. Putremote:on the blocks the orchestrator spawns. cli: claude, written on the block. A remote agent’s session has to be identified by an id orrerix minted before the spawn, and Claude is the only CLI orrerix drives remotely today. Most CLIs recognise a session by scanning a store on the local disk, which a remote CLI’s disk is not;pidoes accept a pre-minted id, but nothing has exercised a remotepiblock, so the rule stays Claude-only rather than widening on an unvalidated path. Leavingcli:off is refused too: an omitted CLI inherits the group default, which is picked at launch, so orrerix cannot tell at load time whether the block would end up on Claude.
Both rules are deliberately strict in the direction that is cheap to change later: relaxing a refusal costs nobody anything, while adding one to a key people have already written into committed files breaks their workflows.
The label itself is letters, digits, - and _, at most 64 characters, and not starting with -. A handful of names Windows reserves for devices (CON, NUL, COM1…) are refused too, in any capitalisation — on Windows those are not file names at all. It is refused rather than cleaned up, so two spellings can never end up meaning one machine — and it is case-sensitive, so buildbox and BuildBox are two different labels, not one written two ways. A block with no remote: key is a local block and is completely unaffected — which is every block in every workflow file written so far.
One thing to know before you write a label into a committed file: a label nobody has bound yet is not an error today. It parses, saves and round-trips, and the block runs locally. That is deliberate for now — the key ships before the binding does — but it means a name written today is a name an operator may bind differently later. Whether an unbound label should say so at launch is a decision for the binding step (#1458); until it lands, treat a label as a note to your operator rather than as a setting that is in force.
The full design note for remote roles lands with the rest of the feature (#1462); the plan it is being built from is on #1436.
Driving a block over its structured protocol: driver: (pi only, in this release)
A block can ask to be driven rather than scraped: driver: structured tells orrerix to run the block’s agent over its CLI’s structured-protocol surface — a JSON stream or RPC channel that reports turns, tool calls, usage and prompts-for-input as events — instead of reading them off a PTY. The value set is orrerix’s own and closed (structured is the only one), and whether a CLI can carry the key is a per-CLI fact the capability table owns: the parse refuses the whole file when the block’s cli: has no structured driver, naming the block, the CLI and the CLIs that can take the key — and in this release only pi does, so a structured block needs cli: pi. (Claude’s stream-json decoder exists, but its spawn-path wiring is a later slice of #84, so its row refuses until then.)
A block that asks for a driver its CLI cannot provide is refused twice: once when the workflow file is parsed, and again at spawn against the CLI the block actually resolved to. The second check is not a belt-and-braces repeat — a block that inherits its cli: from the workflow-level default has no CLI the parser can judge, so that case reaches the spawn path having passed a check that could not see it.
What changes when a block is driven. The pane has no terminal. orrerix talks to the agent over its protocol instead of typing at it, which means turns are delivered as messages rather than pasted and submitted, the agent’s tool calls, reasoning and token usage are reported rather than inferred from the screen, and there is nothing to scrape — so the interactive- question detector and the readiness wait do not run for such a pane, because it can be asked directly instead of watched.
Typing into a driven pane is refused, and says so: a delivery goes through the queue like any other, and a dialog is answered from the pane itself.
Dialogs land in NEEDS-YOU. A driven agent can raise a prompt of its own — a choice, a confirmation, a piece of text — and it blocks until someone answers. orrerix never answers one for you and never lets a timer answer one either: an auto-answer is a decision nobody made, recorded as though somebody had. Instead the ask appears in your NEEDS-YOU queue, and the pane waits. Two exceptions, both deliberate: an orchestrator or manager pane never waits — its dialog is cancelled at once and the ask still reaches your queue, because machine progress must not stop on human absence — and a pane that has piled up more than a few unanswered dialogs has the surplus cancelled the same way.
In this release the driven pane has no pane cell yet. It runs, takes deliveries, logs, and reports its usage and its dialogs; what it does not yet have is the rendered view of that stream in the grid, which lands with #2891. Until then a driven block is visible on the board and in NEEDS-YOU rather than as a pane you can watch.
Telling orrerix a block’s cache TTL: cache_ttl_minutes:
cache_ttl_minutes: on a block tells orrerix the prompt-cache TTL that block’s agent really gets. The pane’s cache-age chip and the orchestrator’s compact-before-idle nudge measure against it:
blocks:
- id: orchestrator
kind: orchestrator
cli: claude
model: opus
cache_ttl_minutes: 60 # this account is on Anthropic's 1-hour cache
Leave it out and orrerix uses the lifetime the pane’s own session shows where its CLI records one (see TTLs below), and otherwise the CLI’s conservative default: 5 for claude and codex, and none for a CLI that routes to several providers. A value you declare here always wins over both. Set 0 to infer no cache state for the block at all. The value is whole minutes, from 0 to 1440 (a day), and anything larger refuses the file: no provider documents a longer cache. A block on a codex model with the 30-minute rule writes 30. The key grants nothing and reaches no command line; it only moves the chip and the nudge.
A manager pane — the human’s own interface
Every block above is an agent doing work. A kind: manager block is not: it is the pane you talk to. Project discussion, “how is it going”, and the half-formed feature idea you have not written down yet all belong there, and its job is to turn the last of those into something the team can build correctly the first time. The manager pane is the page for using one; this section is how to declare one.
version: 1
blocks:
- id: orchestrator
kind: orchestrator
cli: claude
model: opus
- id: manager # at most one per file; a second is a parse error
name: Manager
kind: manager
cli: claude
model: opus
- id: worker
kind: worker
cli: claude
model: sonnet
- id: rev-lead
kind: reviewer
cli: claude
model: opus
gates:
merge:
require: all-pass
reviewers: [rev-lead] # never the manager — it records no verdict
What that block gets you, and what it deliberately does not:
- No fleet traffic is ever delivered into that pane. No notices, no relays, no status lines, no reports — your conversation with the manager is yours. Exactly two things orrerix itself writes there, and both are about the pane rather than about the work: the kickoff that starts the conversation, and — if the CLI compacts mid-session — one re-grounding notice that hands the manager back its own directive ledger, so a direction you gave survives a compact nobody was warned about. Everything else is refused at the front door. News reaches it by pull: the orchestrator posts milestones into a durable mailbox, and the manager reads that mailbox at the start of each of its turns, which is the next time you speak to it. An unread-mail chip on the pane header tells you when something is waiting.
- It holds no authority you have not used yourself. No spawning or killing panes and no review verdicts — those are structural, denied at the tool level. No branches, commits or PRs either: orrerix runs the pane under a containment tier that denies its editing tools, and the rest is its instructions, which is why the designed path has the orchestrator file the issue your brief becomes. It relays; the orchestrator decides.
- It does not start work. A brief it grooms with you becomes a GitHub issue, and your own label on that issue is still the only thing that hands it to the fleet.
- It costs no delegate slot. A manager is exempt from
max_agents, from the idle reaper and from the stall watchdog — it is idle whenever you are not talking to it, which is most of the time, and that is not a fault. - It may not be a gate reviewer, and
prompt:,profile:andallow:are parse errors on a manager block — the same rule anorchestratorblock follows. Its instructions are orrerix’s, not the repo’s. - The group works without it. Close the pane and everything behaves exactly as it does for a group that declares none: the orchestrator takes your input in its own pane, as it always has. Nothing reopens the pane automatically, on purpose — closing it is something you are allowed to do, and orrerix cannot tell that apart from a crash. The group panel says manager declared · not open while it is gone, and the session browser brings it back.
A manager is only ever declared, never inherited. Adding a kind: manager block to the file gives a manager to fresh launches of that repo. A group that already exists — including a dormant one you reattach — keeps the roster its own launch approved and never re-reads the file, so it will not gain one on resume. Launch a new group to pick up the change.
role_hint: liaison is superseded by this. The hint still parses and a repo that uses it keeps working unchanged; the workflow pane now marks it as superseded and the launcher preview badges it LIAISON (SUPERSEDED). Write kind: manager in a new file: a hint on a reviewer block cannot express what this class is — a pane no fleet traffic is delivered into, with a mailbox of its own and a capability set that is not a reviewer’s.
Turning on the merge queue
A merge_queue: block, beside gates:, opts the repo in:
merge_queue:
enabled: true # default false — absent block means the feature is off
max_batch: 3 # how many approved sub-PRs one batch may carry
checks_timeout_minutes: 60 # how long to wait for the batch's checks before
# calling it unverifiable
With it, the orchestrator stops hand-merging approved sub-PRs onto the integration branch and hands them to the queue instead — one batch is tested as a combination, and on red the queue attributes the failure to a single PR rather than leaving someone to guess. With no block, none of that exists and merges work exactly as they always did.
Three things worth knowing before you enable it:
- It never touches your default branch, and never grants what your review gate would not. It lands only on an integration branch, and it re-checks the same reviewer verdicts your
gates:block already declares — at batch build and again at the moment of submit, so afailrecorded in between still stops the landing. It is strictly additive to the gate. checks_timeout_minutesis a backstop, not the mechanism. A batch normally resolves when its checks do. The timeout exists so a repo whose CI never attaches surfaces as unverifiable — loudly, with nothing landed — instead of a batch sitting pending forever.- Adding the block is not inert to older orrerix builds. The workflow file rejects keys it does not recognize, so on a build that predates the merge queue,
merge_queue:fails the parse of the whole file - yourgates:included - rather than being ignored. That is deliberate (a key the build doesn’t understand means you believe a policy is in force that isn’t), but it means everyone sharing the repo wants to be on a build that has it.
The review driver’s driver: block
A driver: block, beside merge_queue:, declares how hard the engine-driven review-loop driver (#1778) may work when it drives a PR through review and CI on the orchestrator’s authority:
driver:
enabled: true
max_review_rounds: 3
max_ci_attempts: 3
max_rebase_attempts: 1
lane_timeout_minutes: 60
fix_timeout_minutes: 60
drive_timeout_minutes: 720
plan_enabled: true
plan_review_minutes: 0
planner_timeout_minutes: 60
fix_nonblocking_rounds: 0
auto_drive_on_done: true
Every number in that example is its field’s own default, so a block naming only enabled: true behaves exactly like the one above. The three switches are the lines the example does not show at their defaults: enabled:, plan_enabled: and auto_drive_on_done: all default to false, and an absent driver: block means the whole feature is off.
A workflow file that stops loading turns both drivers off, and says so once. The drivers read the file every time they act, and a file that no longer parses — a typo, or a key newer than the orrerix build you have installed — reads as no file, which means off. Within about 30 seconds the orchestrator pane gets one line naming the parse error and listing any review drive on disk that will sit still until the file loads again; the same facts are on the audit log as workflow-invalid. It says nothing more for that error. Fixing the file brings the drivers back on the next read, and a different error later is announced again.
Turning the driver off while drives are running also says so once. Setting enabled: false, removing the workflow file, or switching the advanced orchestrator off leaves any unfinished drive listed but never ticked. The orchestrator pane gets one HOLD line naming the cause and the PRs (audit: rd-disabled-with-drives). Turning the driver back on lets the driver pick them up again; cancel_review_drive also needs it on.
plan_enabled is a second switch, not a widening of the first, and it is read UNDER it: the plan driver is off wherever the review driver is. The separation is the consent. Turning the review driver on says orrerix may run a review loop you already had an orchestrator for; it does not say orrerix may spawn a planner and turn its output into work. See The plan driver below for what the second switch actually buys.
| Field | Range | Default | Outside the range |
|---|---|---|---|
enabled | — | false | — |
max_review_rounds | 1–3 | 3 | refuse |
max_ci_attempts | 1–3 | 3 | refuse |
max_rebase_attempts | 0–1 | 1 | refuse |
lane_timeout_minutes | 5–240 | 60 | clamp |
fix_timeout_minutes | 5–240 | 60 | clamp |
drive_timeout_minutes | 5–1440 | 720 | clamp |
plan_enabled | — | false | — |
plan_review_minutes | 0–120 | 0 | refuse |
planner_timeout_minutes | 15–180 | 60 | refuse |
fix_nonblocking_rounds | 0–3 | 0 | refuse |
auto_drive_on_done | — | false | — |
refuse fails the parse of the whole file: a value outside the range is a policy you believe is in force and is not, so orrerix will not load the file at all. clamp pulls the value into range and reports the edit as a warning - the three timeouts are backstops on a wait, and every notify-TTL wait in orrerix clamps the same way.
One drive can take one more review round than max_review_rounds says, and only one. If a round’s blocking fail comes back on a reviewer the driver had re-briefed about the PR body alone - the head had not moved since a full round was already spent on it, and every required reviewer had already answered about that code - the driver hands the worker back once more instead of stopping at the bound. A body fix is a text edit with nothing to build and nothing to re-check, and stopping a drive on one costs you a cancel, a manual worker resume and a re-drive. It is granted once per drive, it is not available for a fail about the code, and the next blocking fail after it stops the drive whatever it is about. review_drive_status shows it as grace_used, and the audit log records it as rd-round-grace.
drive_timeout_minutes is the odd one of the three and its range says so. The other two bound one wait on one fallible signal - a reviewer answering, a worker pushing - which is the same quantity a notify watch’s TTL is, so they take that family’s 5–240. This one is the last-resort bound over a whole drive, and it sits under four per-state bounds that do the real work (below). A backstop measured in the same hours as the waits beneath it is the one clock a drive making steady progress can still trip, and at four hours it did: two drives were parked “stalled” while one of them was mid-round with a finding just fixed and CI green at the new head. Twelve hours is the first figure that cannot be an honest review loop.
A repo may run a tighter loop than the orchestrator template promises, never a looser one: the driver acts on the orchestrator’s authority, and a config file that raised the bound would be loosening the orchestrator’s own invariant. The same forward-compat warning the merge queue carries applies here too: on a build that predates the block, driver: fails the parse of the whole file rather than being ignored.
A restart does not cost a live drive its round. Panes do not survive an app restart and sessions do: on the first tick back up the driver drops every pane its record names that the live roster no longer has, keeps the conversations, and picks the drive up where it was - the reviewer is re-briefed on its own session, a worker mid-fix is handed the fix back, and a drive that was waiting on the CI receipts of a push its worker had already made goes straight on to review at that head. None of it is charged as a review round, and drive_review on such a drive resumes it rather than answering “already driven”.
Non-blocking rounds the driver runs itself (fix_nonblocking_rounds). At the default 0, a gate satisfied with findings still open wakes the orchestrator with GATE SATISFIED, as it always did. Set it to 1–3 and, when every required reviewer passed and every one of them states in its verdict summary that it left zero blocking findings - with at least one stating a non-blocking count above zero - the driver hands the PR back to the worker itself (“review: request-changes, findings on PR #N, address all, report when green”), waits for the report and a green run, re-briefs the reviewers, and repeats. It wakes the orchestrator when a reviewer escalates, when one states a blocking finding or does not say how many it left (an unstated count is never read as zero), when the rounds are spent, when the worker hands back a PR it did not change, and when the gate is satisfied with nothing left open. A fail after one of these rounds takes the ordinary fail hand-back. Each such round is also a review round: the three-round bound is shared, so this never takes a drive past max_review_rounds. The final GATE SATISFIED line says how many rounds the driver ran and what each reviewer stated it left, so the disposition is still yours. Each round is on the audit log as rd-auto-handback, and review_drive_status shows the count as nit_rounds.
A worker’s report(done) can start a drive (auto_drive_on_done). Off by default. With it on, a worker that reports done with ref: #N for its own open PR - the branch orrerix recorded when it spawned the worker is that PR’s head branch - starts a drive on PR #N with that worker’s session, and the report is not typed into the orchestrator’s pane on its own: it rides in the drive’s first notice, and rd-auto-started records it. It is refused, and the report is delivered exactly as before, for a [scratch] PR, a ref that is not a PR, a worker whose branch is not the PR’s head, a PR that already has a drive (running or parked), and a PR that already carries any recorded verdict - a drive starts its round count at zero, which is only true of a PR nobody has reviewed. Every refusal is on the audit log as rd-auto-start-declined with its reason, so you can see why a PR did not auto-drive.
The clean case: every reviewer passed with nothing open. When the gate is satisfied, CI is green, and every required reviewer’s pass at the current head declared open_findings: 0, the drive’s GATE SATISFIED line says clean: true — 0 open findings on every lane: there is nothing for you to disposition. Where the repo turns on the merge queue (merge_queue: enabled: true) the driver also submits the PR to the queue itself, exactly as queue_merge would, and the line ends with the queue’s answer — queued at position N, or the queue’s refusal. The queue still makes every decision it always makes: it re-checks the gate against the recorded verdicts and the live PR, and it never lands on the default branch, so a PR to main is refused base-is-default and stays yours to merge. A reviewer that left open_findings out is never clean, whatever its summary says, and nor is one whose summary states any open finding — 2 non-blocking beside open_findings: 0 is treated as findings left to disposition, not as clean. With fix_nonblocking_rounds on, a reviewer that declares 0 has nothing left for another round, and a declared count is read before the counts in its summary. Each clean gate writes rd-clean to the audit log beside rd-satisfied, carrying the queue’s answer when there was one.
Apart from that one switch, the block enables the feature; it can never target or widen a drive - no other drive exists until an orchestrator makes its own role-gated drive_review call naming one PR. (The workflow pane edits this block too: an enable-toggle whose state is the enabled: line, plus number fields bounded to the ranges shown above - #1869.)
Delivery triage: the triage: block
An orchestrator pane wakes once per notice, and a wake is the single most expensive thing a group does. Most of them do not earn it: on one fifteen-day log, 64 % of 368 orchestrator wakes closed by the LEADING SHAPE of the notice — a review drive reporting GATE SATISFIED, a CI watch that came back green, a planner that posted its plan and exited — with no judgement in them at all (#3304).
A triage: block turns on the gate that stops those reaching the pane:
triage:
enabled: true
provider: none
kinds: []
max_defer_minutes: 30
Every value there but enabled is its field’s own default, so a block naming only enabled: true and provider: none behaves exactly like the one above. An absent triage: block means the feature is off and delivery behaves byte-for-byte as it did before it existed.
What gets held. A notify_when run or PR-checks watch that came back green and whose registered note named nothing to do when it did; a planner that posted its plan and exited; a pane that exited having printed something; a cancelled drive; and the middle chunks of a plan a planner split across several messages. That is the whole list, and it is compiled in — nothing in this block writes a rule.
A drive’s GATE SATISFIED used to be on that list. It no longer is: the notice orrerix emits for one ends Disposition is yours (INVARIANT 3), and under that invariant the disposition is an orchestrator decision — so the rule was wrong on its own terms and was retired (#3324). Triage no longer reads the merge queue at all.
What is never held. Anything whose text names the orchestrator (blocking on you, needs you, your call, is yours), a HELD drive, a blocked report, a watchdog stall, a re-grounding notice, and your own words relayed from a manager or lead pane. And everything else: the default for any shape the rules do not positively recognise is to deliver, so a notice kind a later release adds wakes the pane until somebody teaches triage about it.
Nothing is dropped. A held notice is written to the group’s directory and recorded in audit.jsonl as a delivery-triaged row carrying its full text — that row is permanent, so a notice’s words survive the flush that clears the store. list_deferred() reads what is held right now, which is a different question and answers count: 0 once a flush has gone. It comes back as ONE framed [orrerix] N notices deferred … line in front of the next delivery that DID need the pane, or on its own if nothing does within max_defer_minutes — which is refused outside 1..=240, because the number says how long you are willing to lose sight of your own fleet. A burst also flushes early rather than growing a summary nobody can read.
provider: none is the only value this build accepts, and it is the whole of the privacy posture: with it, no text leaves the machine. Nothing on the delivery path makes a network call — the engine crate triage lives in declares no HTTP client, and neither triage source names one. (orrerix’s own binary does link an HTTP stack, by way of the webview framework; the point is that triage cannot reach it.) A classifier tier for the notices the rules cannot close is #3304 S3, and a file naming a provider today refuses to load rather than running the rule tier while you believe something else is happening.
kinds: narrows which delivery kinds triage may act on; empty (the default) means all of them. The names are the ones list_deferred() reports — drive-gate-satisfied, run-completed, pr-checks, planner-exited, agent-exited, drive-cancelled, message-from, delegate-done, reviewer-report, delegate-progress, delegate-blocked, drive-held, watchdog, system-notice, human — and a name outside that set refuses the whole file, because a misspelled kind is a gate you believe you narrowed and did not.
This block has no form in the workflow pane yet; author it by hand.
The plan driver
The second switch in that block, plan_enabled, turns on a different driver that shares its record and tick discipline. Where the review driver takes one PR through review and CI, the plan driver takes one labelled issue through planning: your orchestrator calls drive_plan(issue), and orrerix spawns a planner from your roster, briefs it, and reads the plan it writes.
The plan arrives as a fenced orrerix-plan block in the planner’s issue comment, and orrerix validates it before anything is posted. An invalid block comes back to the planner as a tool error with line numbers, nothing reaches the issue, and the planner fixes it inside the same turn — which costs your orchestrator no turn at all. Three refused blocks park the drive with the last reasons, so a planner that will not converge stops costing tool calls and starts costing you one notice. Nothing is ever repaired: an id, a branch or a dependency orrerix refuses is refused, never rewritten.
Consent is the label, and it is re-read rather than remembered. agent-ready means build it; agent-investigation means the plan is the deliverable and no worker is ever spawned off it. Take the label off mid-drive and the drive stops with a notice. There is no repo setting that turns that off, and drive_plan on an unlabelled issue is refused.
Turning the switch on also changes what your orchestrator reads. Its instructions gain a plan-drive section — when drive_plan replaces briefing a worker by hand and when it does not, the four tools, what the driver may never do, and how to read the board as a veto — and a group without the switch gains nothing, because prose about a mechanism you do not have is an invitation to go looking for it. Nothing about it is resident: the section is served on demand when the orchestrator is deciding how to plan a work item, so a group that has the switch on pays for the words when it needs them and never otherwise.
Watching a plan drive
Once a plan is stored, an agent-ready drive boards it and runs it. (An agent-investigation one completes right there — the plan is what it wanted.)
Boarding writes one parent row for the issue — reusing one your orchestrator already made, if it did — and one child row per slice, dep-linked to each other exactly as the plan says. The rows are written the way an agent writes them, so your WIP limits refuse them and a dependency cycle is caught, the same as for any other board write.
Then it runs them, one spawn per tick. A slice whose row the board says is ready gets a worker from the block the plan named, on the branch the plan named, in its own worktree. The worker’s brief is a header orrerix writes, then the planner’s own words verbatim, then the same definition of done every other brief in your repo quotes. Nothing rewrites the planner’s text.
When that worker reports done, orrerix hands its PR straight to the review driver — so the first thing you hear about a slice is usually that review driver’s own GATE SATISFIED. A slice’s row is marked done only when orrerix has positively read its PR as merged, and that is what lets the slices depending on it start.
Your board is the veto, and it works while the drive runs. Readiness is re-read from the board every 30 seconds and never cached:
- mark a row
doneby hand and its dependents start, PR or no PR; - set a row
blockedorcancelledand the drive never spawns it, and stops waiting for it; - edit a row’s dependencies and the drive respects them;
- delete a row and the drive stops with a notice naming it, because its dependents can no longer ever become ready;
- a slice the planner flagged
hold: trueis boarded and never spawned — that is the planner telling you it carries a decision that is yours.
What parks one slice, and what parks the whole drive. A worker reporting blocked, a worker whose pane dies without ever reporting, a worker that says it is done but never opens a PR, a PR closed without merging, or a delegate cap that stays full for 15 minutes park that one slice and leave the rest of the plan running; you get one notice per slice. Withdrawn consent, a struck row, and the whole-drive stall backstop park the drive.
By default nothing pauses between the plan and the first spawn. You already pressed go when you put agent-ready on the issue, and an announcement nobody asked for costs your orchestrator a turn. If you want a window, drive_plan(issue, review_minutes: N) — or driver.plan_review_minutes for every drive — posts exactly one notice and waits N minutes before boarding anything, and cancel_plan_drive during it stops the drive with nothing spawned.
plan_drive_status is how you recover a drive after a compaction, and cancel_plan_drive stops one. Cancelling kills nothing: a planner pane that is still open keeps running under your orchestrator, and its reports start reaching that pane again the moment the drive stops being live. Ending a pane is kill_agent, unchanged. resume_plan_drive restarts a parked drive from whatever it was doing when it parked, and gives a re-briefed planner a fresh three attempts rather than resuming straight onto the bound.
The design note is docs/design/plan-driver.md.
Setting up a cross-model reviewer
cli: accepts claude, copilot, gemini, opencode, pi, or codex. So a workflow whose worker runs on Claude gets a genuinely different model family reviewing it by naming one on a reviewer block:
version: 1
blocks:
- id: worker
kind: worker
cli: claude
model: sonnet
- id: rev-gemini # a second opinion from a different model family
kind: reviewer
cli: gemini # model: defaults to gemini's `pro` reasoning tier
gate:
require: [rev-gemini]
You need the CLI itself installed and logged in — orrerix spawns gemini from your PATH the same way it spawns claude. A CLI named by a workflow block is not checked before launch (only the CLIs picked in the launcher’s own role dropdowns are), so if it isn’t installed the pane still opens, prints the shell’s not-recognized error (on Windows, The term 'gemini' is not recognized…), and exits; the orchestrator is then told that agent died. Nothing else is needed: the reviewer’s orrerix tools (including the pass/fail verdict the merge gate reads) are wired up per agent, and its containment is generated per agent too, so a gemini reviewer is denied the file-editing tools exactly like a Claude one.
Two differences worth knowing before you pick gemini for a lane:
allow:doesn’t apply to a gemini block. Those patterns are Claude/Copilot tool-matcher strings. A gemini block runs with its class’s baseline and can’t be widened.- No compact nudge. orrerix’s context-pressure nudge types
/compact, which gemini doesn’t have (its command is/compress), so gemini agents are skipped rather than sent a command that doesn’t exist. - No session history features. orrerix can’t resume a gemini session or read its transcript — gemini mints its own session ids rather than accepting one, so there’s nothing for orrerix to record and reopen later.
cli: opencode is a fourth choice, and it runs opposite gemini on that last point:
allow:doesn’t apply to an opencode block either. Its permission keys (edit,bash, …) are a different namespace from the Claude/Copilot tool-matcher stringsallow:speaks, so an opencode block also runs with its class’s baseline and can’t be widened — same decision, same reason, as gemini’s arm.request_compactworks; the timed nudge and escalation need a window override. orrerix pastes opencode’s own/compactwhen an opencode pane asks for one. But opencode records how many tokens a session holds and not how large its window is, so the lifecycle panel shows an opencode pane’s tokens without a percent, the context threshold never escalates it, and the timed nudge’s context floor refuses it — so the timed nudge skips it while the floor is on — until the group sets a context-window override. opencode also compacts on its own when it runs out of room.- Session history does work. opencode has no
--session-idto hand a pane up front, so orrerix learns which session is the reviewer’s after it starts rather than minting one — but once bound, that session resumes and its transcript reads back like any claude or copilot one.
cli: pi is the fifth choice, and it is the one whose differences are worth reading before you point a lane at it — pi is a deliberately minimal harness, and two of its omissions change what an orrerix block on it means:
- pi has no permission prompts at all, so an attended
piworker still runs every tool without asking. This is the one you want to know before your first run. On every other CLI a non-auto_opsgroup means a human is asked before the agent does something; on pi there is nothing to ask with, so attended and unattendedpipanes get a byte-identical command line and the group’s autopilot toggle is a no-op. Point apiblock at work you would have let run unattended anyway. - A
plannerblock cannot run on pi, and orrerix refuses the file rather than launching one. A planner is read-only, which means denying the git subcommands that commit and push; pi denies tools by NAME (--exclude-tools edit,write) and has no command-pattern deny, so there is nowhere for that denial to live. Arevieweris fine — denyingeditandwriteby name is exactly what a reviewer’s containment is. allow:doesn’t apply to apiblock either, for a stronger reason than gemini’s or opencode’s: pi has no permission engine to pre-approve anything in. Apiblock runs with its class’s baseline and can’t be widened.- Compaction works as it does for claude. orrerix pastes pi’s own
/compact, and the context threshold escalates a pi pane against the window pi’s--list-modelsreports for the model the pane is running. For a model pi does not list, the lifecycle panel shows tokens without a percent, the threshold never escalates it, and the timed nudge’s context floor refuses it — so the timed nudge skips it while the floor is on — until the group sets a context-window override. pi also auto-compacts on its own by default. - Session history works, the claude way. pi takes
--session-idand creates the session if it is missing, so orrerix mints the id up front rather than learning it after boot — no watcher, and a resume is the same flag as a fresh start. - Its cost figures are pi’s own. Tokens come from the session file the same way claude’s do, and the dollars are the ones pi computed against its own provider table — reported, not orrerix’s estimate. A group mixing pi with claude shows its total as mixed, because half of it is a guess and half is not.
- Its orrerix tools ride a community extension, and that extension is not optional. pi ships no MCP support of its own by design. orrerix writes a per-agent MCP config and names it with
--mcp-config, which thepi-mcp-adapterextension reads. If that extension isn’t installed, pi files the unknown flag away without complaining and the pane boots with no orrerix tools at all — it cannotreport, and nothing goes red to say so. Check/mcpin the pane on your first run.docs/design/pi.mdcarries the setup and the one exposure this bridge has (the extension merges your repo’s own.mcp.jsoninto the pane’s tools).
cli: codex is the sixth choice, and the one that comes with a setup step and two things worth knowing before your first run:
- Everything orrerix configures rides one file, in your own Codex home. codex has no MCP flag and no way to append to its system prompt, but it does have
-p/--profile <name>, which layers~/.codex/<name>.config.tomlover your own config. orrerix writes one of those per agent — namedorrerix-<agent id>— carrying the block’s role contract, orrerix’s MCP server, the sandbox posture, and the trust level for the pane’s directory. It is removed when the agent exits, and anything left behind by a crash is swept at startup. This is the only file orrerix writes into another tool’s home directory, so it is namespaced and cleaned up deliberately;docs/design/codex.mdcarries the argument for every key in it. - Your own
[mcp_servers.*]entries are still there, and orrerix’s layer wins a name collision. The profile is a layer over~/.codex/config.toml, not a replacement for it, and codex’s TUI has no exclusive-config switch. So a codex pane sees your MCP servers as well as orrerix’s — which is usually what you want — and if you happen to have a server namedorrerix, the pane gets orrerix’s rather than yours. Every spawn that finds any records what it saw in the audit log (codex-user-mcp-merged) so a pane whose tools look wrong is diagnosable rather than mysterious. - Compaction works as it does for claude. orrerix pastes codex’s own
/compact, and the context threshold escalates a codex pane against the window codex records in its session log. A session log that records no window shows tokens without a percent, the threshold never escalates it, and the timed nudge’s context floor refuses it — so the timed nudge skips it while the floor is on — until the group sets a context-window override. codex also compacts on its own when it nears its limit. - A
reviewer, aplannerand amanagerblock cannot run on codex, and orrerix refuses the file rather than launching one — see Why not codex for a reviewer? below. All three are classes orrerix denies the editing tools to, and codex has no way to deny them. allow:doesn’t apply to acodexblock, for gemini’s reason and one of its own: codex’s rules engine can forbid a command prefix, but it loads rules only from~/.codex/rules/and the repo’s.codex/rules/, neither of which is per-agent. Acodexblock runs with its class’s baseline and can’t be widened.- The posture toggle is real, and it lives in the profile rather than on the command line. An unattended (
auto_ops) codex pane runsapproval_policy = "never"; an attended one runson-requestand will raise an approval overlay, which orrerix’s attention scan picks up. This is the opposite of pi above: the two postures produce an identicalcodex …command line precisely because the difference is in the file. orrerix’s own tools (report,message_orchestrator, …) never prompt in either posture: the profile pre-approves that one server’s tools, since an agent that has to ask before it can report is one nobody hears from. - The sandbox is
workspace-write, with network on. Edits inside the workspace, and network access explicitly enabled because a worker that cannot reach GitHub cannot open a PR. codex’sread-onlyrung is not a middle ground — it blocks running commands and the network too — and orrerix never emits the bypass flag. On Windows, codex’selevatedsandbox runs commands as a separate local account, which cannot read the GitHub tokengh auth loginstored in your credential store — so orrerix reads it once per codex pane (gh auth token) and hands it to that pane asGH_TOKEN, in the pane’s environment only, never in the profile file. That makes the token readable by anything the pane runs, as it already is in a Claude or Copilot pane; and it is read when the pane starts, so aftergh auth refreshor a logout, respawn a long-running codex pane to pick up the change. If that read fails the pane still starts, withoutghaccess, and the audit log recordscodex-gh-token-unavailablewith the reason. Ashell_environment_policyof your own that filters*TOKEN*variables (codex’signore_default_excludes = false, orinherit = "core") strips it again. - A codex worker in its own worktree can commit and push. A git worktree keeps its metadata inside your main clone’s
.git(under.git/worktrees/<name>), outside the worktree, and codex’s sandbox keeps.gitread-only by default — so without help a codex worker could edit files but nevergit commit. For a pane in a linked worktree, orrerix’s profile adds that worktree’s own git directory and theobjects,refsandlogsdirectories of the shared.gitto the sandbox’s writable roots — what a commit, a push and a rebase write. It does not open the shared.gitas a whole:hooks/andconfigstay read-only, because a hook or a config key written there would run as you the next time your git runs. The worktree’s own git directory has to be writable, but the three files in it that steer git (commondir,config.worktreeandgitdir) are kept read-only. So the pane can’t point your git at a different.gitor give it config, either. To do that, a codex pane in a worktree runs under a codex permissions profile namedorrerix-worktreeinstead of the plainworkspace-writesetting, and orrerix creates an emptyconfig.worktreein that directory if there isn’t one, so the read-only rule has a file to apply to. One route stays open: the pane can write the state of a rebase in progress, so don’t continue a rebase (git rebase --continue) in a codex pane’s worktree that you didn’t start. Keeping the shared.gitread-only costs two things.git push -upushes but printscould not lock config fileand does not record the upstream, so name the remote and branch when pushing (git push origin <branch>). And deleting a branch that git has packed intopacked-refs(git branch -D) fails. A pane whose directory is your main clone gets nothing extra, so a codex pane there can’t commit; nor can a solo codex pane, which is yours to approve. If a worktree’s git layout isn’t the onegit worktree addmakes, orrerix adds nothing and recordscodex-worktree-gitdir-unrecognisedin the audit log with the reason.docs/design/codex.md(Committing from a worktree) has the argument and what stays open. - Session history works, the copilot way. codex has no flag that pre-assigns a session id, so orrerix watches your
~/.codex/sessionsstore for the rollout the pane creates and binds it afterwards. If two codex panes in the same directory both start a session before either is identified, orrerix refuses to guess between them and leaves both unidentified — recorded assession-untrackedin the audit log — rather than binding one pane to the other’s conversation. - No model is set unless a block asks for one. The
modelkey in your own~/.codex/config.tomlis what a codex pane runs on; orrerix’s profile deliberately names none. A block that wants something specific pins its ownmodel:.
Why not codex for a reviewer? codex can’t deny its editing tool by name, and its sandbox is all-or-nothing — strict enough to block the tests and gh a review needs, or open enough to let the reviewer rewrite the code it’s reviewing. Its rules engine could express the missing denial but cannot be scoped to one agent. A reviewer that can’t be contained would quietly weaken the merge gate, so orrerix refuses the pairing rather than shipping it. The same reasoning refuses a planner, which is read-only and needs strictly more, and a manager, which sits at the reviewer’s tier for the same purpose: it reads the codebase to ground its questions and must not write it.
Turning it on live shows the same resolved-roster confirm (name, blocks, any declared gate) the launcher’s own preview shows at launch time; turning it off confirms that future spawns fall back to the built-in roster on your default CLI (per-role CLI overrides picked at launch aren’t separately retained, so an off→on→off round trip rebuilds the roster from your default CLI rather than restoring them).
Adding a second lens
A repo that wants a design-review opinion before code is written, or a premortem pass on an unusually risky PR, doesn’t need new capability to get there — kind: planner + role_hint: advisor already gives a block the right shape: read-only, reports with report("done", ...) and exits rather than holding a pane open, and never records a verdict. What it does not already give you is the trigger. Every sentence orrerix itself generates about an advisor block — the orchestrator’s own kickoff note, the worker’s counterpart, and the non-overridable addendum the advisor block receives regardless of its persona — is keyed to the team being stuck, not to plan intake, PR size, or risk. A workflow file cannot widen that: prompt:/profile:/allow: on the orchestrator block are a parse error (see “The orchestrator block is loomux-owned” in docs/design/workflows.md), so there is no way to tell your orchestrator “consult design-review at plan intake” or “spawn premortem when the diff looks risky” from the file itself. In practice, what fires a plan-intake or high-risk consult today is a human asking for one, or the orchestrator’s own judgment reading the roster — not anything this pattern wires up mechanically. Declare as many advisor blocks as you want, each with its own persona; just don’t expect the trigger to come for free.
Why not kind: reviewer. orrerix’s own built-in orchestrator template makes the orchestrator run every reviewing block — every kind: reviewer block except one hinted role_hint: liaison — on every PR, unconditionally, whether or not that block is named in a merge gate. Declaring the lens as a reviewer costs a pane on every PR it was never meant to see; naming it in a gate on top of that adds a second, separate cost — it can then hold every merge shut until someone spawns it and it passes. An advisor block avoids the first cost outright (nothing spawns it automatically) and the second is structural, not just a discipline you have to keep: gate_reviewer_error refuses, at parse time, any gate naming a non-reviewer block — an advisor cannot even be named in one, the file would not load. The orchestrator only spawns it when it judges the moment warrants the extra look (see the trigger caveat above).
Two ready-made personas, modeled on this repo’s own .github/agents/advisor.md:
---
name: design-review
description: >
A read-only advisor consulted on a plan before it's built, or a PR whose
diff crosses this repo's size threshold — a second opinion on the shape of
the change, not a correctness review. Investigates and reports; never
merges, spawns, or records a verdict.
kind: planner
mode: replace
---
You are consulted on a design question: a plan under intake, or a PR the
orchestrator judges large or consequential enough to warrant a second look at
its shape, not just its correctness.
## What you do
1. **Investigate READ-ONLY.** Read the plan or diff, the issue thread, and
any design notes. You cannot write a file, branch, or push — the planner
capability class denies those at the CLI level regardless.
2. **Answer one question: what alternative did this implicitly reject, and is
the choice defensible?** Name the alternative, not just "this looks fine"
— a design review that agrees with everything isn't a second opinion.
3. **Report and exit.** `report("done", "<your assessment>")` is your one
deliverable — lead with the alternative and your verdict on it. Skip
anything that didn't change your answer.
## What you never do
No authority beyond the assessment: never merge, never spawn another agent,
never record a review verdict, never edit or push. The orchestrator decides
what to do with your advice, including ignoring it.
---
name: premortem
description: >
A read-only advisor consulted on a PR the orchestrator judges high-risk —
a migration, a security-sensitive surface, a persisted-shape change.
Investigates and reports; never merges, spawns, or records a verdict.
kind: planner
mode: replace
---
You are consulted on a PR the orchestrator has flagged as high-risk. Your job
is to imagine it has already shipped and failed, and work backward.
## What you do
1. **Investigate READ-ONLY.** Read the diff, the issue thread, and the tests
it added. You cannot write a file, branch, or push — the planner
capability class denies those at the CLI level regardless.
2. **Answer three questions:**
- **Premortem** — two concrete ways this fails in production that no test
in the PR catches, or an argued "none".
- **Resource envelope** — if the diff touches an unbounded input, the
largest realistic size × invocation frequency × allocation/IO, and
whether anything bounds it.
- **Operational futures** — what happens at 10× load, and on an
upgrade/rollback across whatever persisted shape the diff touches.
3. **Report and exit.** `report("done", "<your findings>")`. An empty answer
to one of the three questions is a finding in itself — say so rather than
skipping it.
## What you never do
No authority beyond the findings: never merge, never spawn another agent,
never record a review verdict, never edit or push. The orchestrator decides
what to do with your findings, including ignoring them.
workflow.yml declares both the same way as any other advisor block:
blocks:
- id: design-review
name: Design review
kind: planner
role_hint: advisor
profile: .github/agents/design-review.md
- id: premortem
name: Premortem
kind: planner
role_hint: advisor
profile: .github/agents/premortem.md
edges:
- { from: orchestrator, to: [design-review, premortem] }
spawn_agent is orchestrator-only for every block, advisor or not — a worker that gets stuck is told to message_orchestrator and ask for a consult rather than spawn one itself — so both lenses hang off the orchestrator in edges:, never off a worker or reviewer. (One implementation detail worth knowing: the orchestrator’s auto-generated “consult the advisor” kickoff sentence is built by role_hint_block, which the engine’s own doc comment says picks the first block carrying a given hint on purpose — so with two advisor blocks declared, that sentence names only the first. The second is still listed under “Your delegates” with its own persona, and is spawnable by block: "premortem" exactly the same way — it just isn’t individually called out by that one sentence.)
The residual. An advisor’s report can never satisfy or block a merge gate — not just because it never calls review_verdict, but because a gate cannot even name it (see the structural point above), so the orchestrator reads its advice and dispositions it like any other input, the same as a human’s. A repo that wants these questions to actually gate a merge, not just advise on one, puts them in its standing reviewer persona instead, as fixed headings its review body must carry (this repo’s own .github/agents/rev-lead.md does exactly that for its own question set): that shape buys enforcement, at the cost of running on every PR rather than only the ones that need it — and it needs no trigger caveat, either, since a reviewer that’s named in the gate is spawned every time by construction.
Proposed lessons come with their evidence
A workflow can declare a process-pro block — a worker that runs after a PR merges, reads that session’s record cold, and opens a normal PR proposing a durable lesson (an entry in .orrerix/lessons.md, a .claude/skills/ entry, a CLAUDE.md rule). Like every other agent it proposes and stops; it never merges anything, its own PR included.
Unlike every other agent’s PR, though, this one does not come to you. The learning loop is meant to be self-managed, so a process-pro PR is orchestrator- owned: the orchestrator reviews it and then merges or closes it itself, rather than parking it in your merge queue. That is deliberate — a loop whose whole output is “here is another PR for the human to read” costs you more attention than the lessons are worth, and stops running the week you get busy. The bar it merges on is the ordinary one, not a lower one: the group’s review passed, CI green, findings settled. Only the owner of the decision changes.
Two things that follow. Closing is a normal outcome — most sessions produce no durable lesson, and an orchestrator that merges every proposal is not filtering. And you are not out of the loop: each merge is audit-announced and recorded on its board task, so the lessons that landed while you were elsewhere are there to read back, and a lesson you disagree with is a curation PR away from being gone.
The thing worth knowing when you do read one is what it is allowed to claim. Anything the process-pro writes into those files is inlined into every future session’s context, so a wrong or trivial lesson is a cost you keep paying — which makes “was this actually a recurring problem, or did one agent have one bad afternoon?” the question the review turns on.
orrerix answers it mechanically rather than leaving it to the agent’s opinion of itself. Each piece of friction it found carries a recurrence count: how many other sessions in the group hit the same wall, and which ones. So a proposal should read like “three sessions hit this — w-2, w-7, w-9“, and you can go look at those sessions. A proposal from a wall only one session ever hit is supposed to say so and argue why it will recur anyway (a documented rule somebody missed, say) — if it doesn’t, that is the cue to close it rather than merge a lesson built on one bad afternoon. The orchestrator is the one holding that cue on a normal run; it is also what you check for if you go back over what the loop merged.
Two caveats the proposal should carry when they apply, because they change what the number is worth: a brand-new group has no earlier sessions to compare against, so a 0 there means nothing to compare, not never happened; and only a bounded number of recent sessions are read, so on a long-running group a count is a floor rather than a total.
What actually reaches a kickoff from .orrerix/lessons.md
The lessons file is injected into every orchestrator kickoff, and only about 4 KB of it is — a deliberate bound on how much repo prose lands in an agent’s context (agents get the file as data to weigh, never as instructions). Files outgrow that, so what happens at the edge is worth knowing:
- Whole entries are dropped, oldest first. The unit is a
##heading and its body; you never get half an entry, or a body injected under the wrong heading. A##line inside a fenced code block doesn’t count — an entry can quote a heading (including this page’s[pinned]example) without being split in two. - The injection says what it dropped. The first line names each evicted entry by heading, so “the rule about X isn’t reaching agents any more” is visible in the kickoff instead of being something you find out the hard way.
[pinned]in a heading keeps that entry. Put it in the##line — e.g.## Never resize the PTY for a UI feature [pinned]— and eviction takes it last, after everything unpinned, whatever its position in the file. Use it for the entries whose absence is a real failure; if the pinned entries alone exceed the cap, the oldest pin is dropped too, so pinning everything is the same as pinning nothing.
Dropping is not deleting: the file is untouched on disk. When entries start falling out, the fix is a curation PR that retires the stale ones — the same review path any other change to the file takes.
Watching the merge queue
A repo can turn on a merge queue (merge_queue: in its .orrerix/workflow.yml) so a batch of approved sub-PRs is tested together on a scratch ref before any of them reaches the integration branch — the combination is what gets a gate, instead of each PR getting one and nobody checking the pile. The queue runs in orrerix itself and lands only on an integration branch, never on your default branch; see docs/design/merge-queue.md for the design.
The lifecycle panel shows what it is doing, and nothing more — the row is read-only. There is no button here to enqueue, cancel, or land anything: the queue is host-run, and the orchestrator drives it through its own tools. What you get is the branch the queue is landing on, the batch in flight (with the draft PR whose checks it is watching), and one line per queued PR:
- queued — waiting for a batch. If a PR was rebased after its reviewers passed, its approvals no longer cover its head, so it is queued and blocked, and the row says why. It becomes eligible again the moment a re-review covers the new head; nothing merges unreviewed.
- in the batch being built · waiting on batch CI · landing — in flight.
- in the bisect · kicked back — the batch went red and the queue is attributing (or has attributed) it. A kicked-back PR is not going to land as things stand; its own PR carries a comment naming the failing check.
- landed · cancelled — done with.
Two things the row will never do quietly. If more PRs are queued than fit, it says showing 6 of 12 entries rather than showing you six and letting them read as all of them. And if orrerix cannot read the queue’s state file at all — a torn write, or a file written by a newer build — it says that, loudly, instead of drawing an empty queue: “nothing is queued” and “orrerix can’t read the queue” are the same picture otherwise, and only one of them means your PR is fine.
How quickly it moves. The queue is driven by orrerix’s background poller, which wakes every 30 seconds and advances one group’s queue per wake — so a batch normally starts within a wake or two of the last PR being queued, and a batch whose CI has just gone green lands about that fast too. If several groups have live queues they take turns, oldest first. That bound is deliberate: the same loop delivers every agent’s notify_when CI notice, and a driver that serviced every group on one wake would hold those up. If something external fails — the remote is unreachable, a push is rejected — the queue holds that group off for five minutes rather than retrying every wake, so you get one notice about it rather than ten.
The row is absent entirely until a group actually has a queue, which is the default: no merge_queue: block means the feature is off and nothing about your group changes. See Turning on the merge queue for the block itself.
What the orchestrator does with it. It queues an approved sub-PR rather than merging it, reads the queue’s state, and can pull a PR back out — three tools, no merge authority beyond what it already had. When a batch goes red it gets one notice naming the culprit, and a comment lands on that PR with the failing check and the batch’s sibling set. orrerix deliberately does not brief the PR’s author itself: attribution is mechanical, but deciding who picks it up — and whether to resume that worker or spawn a fresh one — is a judgment call, so it stays the orchestrator’s.
Two honest limits, both of which the culprit comment states in as many words. Bisect isolates a culprit, not necessarily the culprit: when two changes are each fine alone and only fail together, the search blames whichever one the split isolated, and the comment names the siblings so you can see that rather than being told a half-truth confidently. And a batch that comes back unverifiable implicates no PR at all — the checks never resolved, nothing landed, and the thing to look at is your CI.
Watching a review drive
A repo can also turn on a review driver (driver: in its .orrerix/workflow.yml) so the worker-reviewer rounds a PR goes through — wait for CI, brief the reviewer lanes your gate requires, hand a fail or a red run or a conflict back to the worker, repeat — run in orrerix instead of costing the orchestrator a turn each time. It runs on the same 30-second poller the merge queue does, under the same bound: one group per wake, oldest first. See docs/design/review-driver.md for the design. The driver: block’s own fields are documented with the other workflow blocks by #1784, which lands beside this — until it does, this page describes what the driver does and not what you may set.
Nothing starts by itself unless you ask for it. The block only enables the feature. No PR is driven until the orchestrator makes a deliberate per-PR call naming that PR and the worker session that owns it — and by default a drive does not start when a worker reports it is done, because the PRs where a drive would be wrong are ordinary ones: a scratch PR, a release bump, a PR you said you would read yourself. The one exception is opt-in: driver.auto_drive_on_done (above) lets a worker’s report(done) on its own PR start the drive, and it still refuses a [scratch] PR and any PR already reviewed. A release bump or a PR you want to read yourself is not something orrerix can tell apart, so leave the switch off in a repo where those come from workers.
The call checks that the session can actually take a hand-back. drive_review refuses at the moment of the call — with the same line the hold would have printed — when the session you named is the orchestrator’s or the manager’s own, or the roster no longer declares the block that session was minted under. Before that check existed, such a call was accepted and then held worker-unresumable on every resume: three holds and three orchestrator turns for a PR that could never be handed back, which is why the check is at the call rather than at the first failure. And when a hand-back does fail the same way twice — same session, same failure — the second hold’s quoted refusal is prefixed second time, because the remedy the first notice suggested (a session that resolves) is the one that just failed: re-point the drive at a different session, or cancel it.
The driver never merges, and never grants what your gate would not. It cannot merge, push, mark a PR ready, delete a branch, edit or relabel a PR or an issue, write a merge grant, kill a pane, or record a verdict — only a reviewer’s own verdict opens your gate, and a finished drive is never a substitute for one. It reads GitHub and it types templated text into panes orrerix already owns. What is genuinely new, and worth saying plainly: orrerix now spawns a reviewer and resumes a worker on its own initiative, with no orchestrator turn in between. Every one of those actions is in the audit log, marked with the orchestrator it acted for.
One thing changes about what you see in the orchestrator’s pane. While a PR is being driven, its delegates’ status reports and recorded verdicts go to the driver instead of appearing there — that is the point, and it is most of the saving. One qualification, because you will otherwise notice it and wonder: interception is keyed on a pane orrerix has recorded, and it records the worker’s pane when it first hands the PR back. Until that first hand-back — while the drive is still waiting on CI or on a reviewer — a report from the worker still lands in your pane as it always did. Nothing in the drive reads it, so nothing is lost; it is simply not yet silent. Two things keep it from being a black box: every consumed event is in the audit log (as rd-consumed, naming the kind, the agent and the PR — consumed is a different word from dropped), and a delegate’s own message_orchestrator line is never intercepted. If a reviewer or a worker has something to say that is not a status change, you still see it, unchanged, and the drive then stops so someone reads it.
A driven worker that reports progress gets answered, in its own pane. A drive advances on a worker’s report(done) — including the case where the fix moved nothing to push, such as a PR-body edit or a finding the worker answered rather than changed code for. A report(progress) moves it no further, so instead of waiting the fix timeout out the driver types one line back into that worker’s pane saying so. Once per hand-back, never to you.
After a hand-back, green checks are not on their own enough to open a review. A worker can only fill its PR body’s CI receipts — run ids, what each platform concluded — once the checks have settled, so a reviewer briefed the moment the matrix went green was briefed against a body the worker was about to edit, and the pass it then recorded was stale before it was written: the gate blocks on body-unchanged, and a whole re-record round buys nothing but a digest match. So on a head the driver handed back and the worker pushed, it waits for that worker’s report(done) as well before opening the lane. What you see while it waits is a drive sitting in ci-wait on a green PR, which is the driver holding the reviewers until the revision stops moving. A worker that pushes and then goes quiet is bounded like any other: fix_timeout_minutes from the push, and then the drive parks as fix-stalled with a line naming the pane to read. A worker that reports blocked, or a pane that could not be reached, is answered by its own hold at once rather than waited out. None of this applies to a drive that has not handed anything back yet — the first pass over a PR you drove after your worker already reported is unchanged.
A pane a live drive is using is marked, and killing it takes a deliberate override. The driver holds panes between rounds — the worker it handed the fix to, the reviewer lanes it opened — and an idle one of those looks exactly like a delegate to reclaim. It is not: killing it strands the drive until the next tick notices and parks. So list_agents marks each such row driven_by: "#<pr>", and kill_agent refuses one, naming the PR and both ways out — cancel the drive first, or pass force: true. The field has exactly three shapes and is always present: "#<pr>" when a live drive is using that pane, null when none is, and "unreadable" when orrerix could not read this group’s drive record at all — which is not the same as “nothing is driven”, and is why kill_agent refuses on it too. This matters most at the live-delegate cap, whose own refusal says to kill an idle delegate and lists who is idle: that list now marks the rows the advice does not apply to. If you do force it, the drive says what it cost on its next tick — the hold names the pane and reads (ended by orchestrator), so the outcome comes back to whoever chose it. Closing a pane yourself, from the UI, is untouched — this is the orchestrator’s tool, and orrerix does not refuse you your own panes.
A finished drive means gated AND mergeable. A PR can go CONFLICTING at any point — someone else’s branch lands on the base while yours is being reviewed — and the driver now reads that wherever it sees it, not only while it is watching CI. Before, a conflict that arrived after the lanes had passed was reported as a satisfied gate, because the gate genuinely was satisfied and only the merge was not; and a conflict that arrived mid-review parked the drive saying it could not work out which reviewers were required, which is true (GitHub computes no file list for a conflicted head) and is a symptom rather than the problem. Both are now the same thing that a red run is: the worker is handed back and asked to rebase, on its own one-attempt budget, and a second conflict parks the drive as rebase-limit. A mergeability GitHub has not finished computing is not a conflict and changes nothing. A reviewer still open on that head is stood down rather than left to finish — one line is queued into its pane asking it to stop and record no verdict (queued, never an interrupt: it reads it when it next takes input), and its pane is freed once it reports. Its conversation is kept for the re-brief at the rebased head, and no review round is charged for the reading it never got to deliver.
A drive stops, it does not drift. There are eighteen ways out and each produces at most one line in the orchestrator’s pane: the gate being satisfied, the drive being cancelled — by you, or by orrerix on its own when it sees the PR has been closed — or one of sixteen holds — a counter reaching INVARIANT 9’s bound, a reviewer escalating, a lane or a worker going quiet past its timeout, the drive sitting in one state past that state’s bound, the drive itself getting old, a reviewer requirement orrerix could not compute, a gate file it could not read, a worker that reported blocked, a delegate messaging the orchestrator, this group’s live-delegate cap refusing the pane a hand-back needed, that same cap refusing a reviewer for long enough that the drive cannot get started at all, a fix that could not be handed back to its worker, or the account behind one of the drive’s panes running out of budget.
That last one is worth knowing about separately, because it is the only hold that costs the drive nothing at all — no review round, no CI attempt, no timeout. When a model provider stops a pane (an OpenRouter key at its spend cap, Claude usage credits exhausted), orrerix reads the refusal off the pane’s own text and parks every drive whose panes are on that provider, on the next tick rather than an hour later. Because one limit stops every pane on that provider at once, you get one line naming the provider and listing the PRs held, not one per drive. Fix the billing — or point the block at a different model: — then drive_review each PR to restart it. orrerix does not restart them for you: a pane going quiet again is not proof the account was topped up.
A drive waiting on a worker survives a restart of orrerix. Panes do not — they die with the process — so on the first tick after a restart the driver re-briefs that worker by resuming its recorded session, and the drive carries on where it was. It costs the PR nothing: no review round, no CI attempt, no timeout, and the wait for the fix is measured from the brief the worker can actually answer rather than from the one that went down with the old pane. If the session really has been lost, that attempt is what discovers it, and you get the fix-handback hold below instead of an hour of silence.
The fix-handback hold — a fix that could not be handed back to its worker — quotes what actually refused rather than diagnosing one cause: the session may no longer resolve, the block it was minted under may no longer be declared in this group’s roster, or the pane the driver resumed may have opened and then exited without saying anything — and when the same hand-back has failed the same way twice, the line says so (second time), since the remedy is then a different session or a cancel, not another resume. The cap one (cap-refused) is deliberately separate from it, because its remedy is: the recorded session is fine and what is exhausted is a slot, so freeing one — kill_agent on an idle delegate, and the notice names which are idle — is what clears it, not re-pointing the drive at a different session. It is the one hold reason named here by its own word, because it is the one whose remedy is a different action from its neighbour’s.
“At most one”, because two of those exits are deliberately silent — and both are readable. A drive you cancelled with cancel_review_drive is not announced: that call already answered you, synchronously, with the panes it released, so a prompt arriving afterwards would be a wake-up about something you are holding. A cancel orrerix decided on its own — it saw the PR closed or merged — still lands in your pane, because nothing else would tell you. And a hold that repeats one the drive has already announced — the same LINE, word for word, which is what a resume that changed nothing produces — is not sent a second time. Anything the line says differently still reaches you, a changed refusal included. The two holds whose line reports how LONG the drive sat (state-stalled, drive-stalled) are the exception and always reach you: a second one means it sat out the whole bound again, so the repeat is news. Both are on the audit log with the full text of the line (rd-notice-demoted, rd-hold-repeated), and a parked drive is listed by review_drive_status whatever its notice did. A hold at a NEW commit always announces: the head is part of what makes two holds the same hold, so a resume after your worker pushed is a hold about different code and you hear about it.
The notices themselves are short on purpose. The line that says a gate is satisfied names the lanes and their verdicts, what the drive spent, how many lanes left non-blocking findings, the panes it released, and list_verdicts("<pr>") — it does not re-send the reviewers’ own summaries, because reading them is what that call is for and every byte in your pane is paid for again on every later turn until you compact.
A drive the cap starves says so, in fifteen minutes rather than hours. A reviewer spawn refused by the live-delegate cap is normally nothing to act on — another drive’s lane finishes, a pane is closed, and the next tick spawns — so the drive simply retries. But a group whose slots are all held goes on refusing, and before this the drive sat in review-wait with no lanes and no line in your pane until its total age bound expired: the longest measured was about three hours, and nothing on screen said why. Now a refusal that lasts a quarter of an hour parks the drive as cap-full, whose line names what to free and what to run afterwards. It is a separate word from cap-refused above even though the remedy is the same, because the two say different things about how long: one is a single refusal on the spot, this is a run of them, and telling a drive that has just hit the cap from one that has been stuck all afternoon is the whole point.
The driver releases the panes it is finished with, and nothing else. A reviewer whose verdict is recorded at the commit the PR is on now has answered the only question the drive asks it, and a worker that reported done has had that report read and acted on. In both cases the pane is idle, what it produced is already on disk, and the conversation is kept — so the driver closes the pane, the slot frees immediately, and the next round reopens that same session in a fresh pane. Nothing is lost but the pane: a reviewer asked again comes back to a PR it has already read, exactly as it did before.
A worker’s report counts wherever it lands. The usual round is push, then wait for the checks, then report — so the report reaches the drive while it is watching CI rather than while it is waiting for the fix, and for a while the release only fired on the other ordering (a body-only fix, with nothing to push). The measured effect was that three quarters of hand-backs kept their worker pane through the whole of the next review round, and the orchestrator ended up killing them by hand. Both orderings release now; the drive holds the worker pane only while it is actually waiting on that worker.
And a drive that ENDS releases before it tells you it ended. A satisfied or cancelled drive used to list its panes for you to dispose of, which is a turn spent doing what the drive could do itself — so the last lane and the worker go first, and the notice names what is really left. The worker’s line then reads worker session <id> resumes with spawn_agent(resume:): the pane is gone, the conversation is not, and that session id is what brings it back.
It kills nothing else. A reviewer that has not answered, one whose verdict belongs to an older commit, a worker that is still working or that reported blocked, a worker that still owes a round to a drive being cancelled under it, a drive that is being PARKED by the move it is making this tick — all keep their panes, and so does every pane in the group that is not this drive’s. cancel_review_drive releases nothing either: you called it, so the panes are yours, and its result lists them. The driver never goes looking for a pane to free because it is short of slots: it releases on facts about the pane, never on how full the group is. Killing anything else is still yours or the orchestrator’s — or the idle reaper’s, where you have set one — and cap-full is still the sentence a drive owes when it is starved, with its line listing the panes that drive still holds so you can see whether the pressure is even its own. What changed is how often that happens: a drive between rounds now holds no reviewer slot at all.
When it releases a worker it releases every pane it still holds on that session, not only the one it resumed last — a pane an earlier hand-back superseded was otherwise left sitting idle in a delegate slot for the rest of the drive. The same per-pane test decides each one, so a superseded pane that is busy stays exactly as a busy current pane does.
When a drive ENDS — gate satisfied, or the PR closed under it — it also releases the pane you handed it when you started it, if that pane is idle. That is the one it used to leave behind: a drive that never had to hand a round back owns no worker pane of its own, so it finished with your original worker still idle on the worktree and you had to kill it by hand before the worktree could be removed. It is the pane (or panes) that were live on that session when you called start_review_drive, recorded then and not looked up again — so a pane you open or resume onto the session while the drive is running is never on the list, including one you open in the moment between the last check and the exit. A pane another live drive opened for itself is left to it, and a busy pane is left alone; no release row claims otherwise. A pane that is left alone is NAMED in the ending notice, beside the ones the drive still owns, so what survived the drive is something you can see rather than something you have to go looking for — that includes the cancel a restart performs for a PR that closed while orrerix was down, which releases nothing at all. Mid-drive nothing changes: a pane the drive never spoke to may still be one you are using, so only the panes it opened itself are released then.
Each release is on the audit log as rd-lane-released or rd-worker-released, naming the pane, the session kept and why — so it costs you no line in your pane and is still there to count.
A drive is bounded by where it is stuck, not by how long it has existed. Each of the four working states has its own bound, and it starts again from zero every time the drive moves: 90 minutes in fix-wait, and 15 minutes in gate-check, which is a decision rather than a wait. The other two are not flat numbers, and for the same reason — each holds more than one wait end to end, so its bound has to cover their sum. ci-wait is 90 minutes plus one fix_timeout_minutes, because after a hand-back it waits for the checks and then for the worker’s report — two and a half hours at the defaults. review-wait holds your reviewers one after another, so its bound is three hours plus one lane_timeout_minutes per required lane — four hours for a one-lane gate at the default, six for three. Past one of these the drive parks as state-stalled, and its line says which state, how long it sat there and what the bound was — so resuming it is a decision you make rather than a reflex. None of these numbers can overrule a driver: value you set: raise lane_timeout_minutes or fix_timeout_minutes and the state bound above it rises with it. And the holds that name a real remedy — lane-stalled, fix-stalled, cap-full, each of which can tell you which pane to read or which slot to free — all fire well inside these, so this is the catch-all rather than the usual answer.
The total age is still there, as the backstop, at twelve hours. It is what catches the one drive the per-state bounds structurally cannot: one that advances on every wake and never finishes, such as a PR whose gate requires a green default branch that stays red — it resets every state clock for ever, so only the whole-drive clock can see it. That hold is drive-stalled, and its line now says the same three things, plus that it is the backstop, so “the drive kept moving and never finished” cannot be mistaken for “the drive sat still”.
Neither clock charges a drive for time it could not act. While this group’s live-delegate cap is refusing the drive a reviewer, both clocks are paused — a hold is not progress and it is not a stall. review_drive_status publishes both halves rather than netting them: since_ms stays the plain wall age, starved_ms is how much of it the drive spent unable to spawn, and state_ms is the clock the state bound actually reads.
A hold is parked, not finished: it keeps what it has spent, so resuming it does not silently grant a fresh budget, and clearing the counters is a separate, audited decision.
A hand-back reopens the worker’s own session under the worker’s own block — the persona and CLI that session was minted with, never the roster’s default worker block. That matters wherever a workflow file declares more than one worker: a session belongs to one CLI, and reopening a Claude transcript under an opencode block does not produce a different persona, it fails to open at all. If the block a session was minted under is no longer declared, the drive holds and names it rather than quietly resuming the work as somebody else. Where that session already has a live, idle pane running that same block and that pane is ready to be typed into, the driver types the brief into it instead of opening a second pane on one conversation — so a review round normally costs no new delegate slot at all. “Ready” is a fact about deliveries, not about what is on the screen: the last thing orrerix typed into that pane is on record as having landed, and nothing is still queued for it. A pane that is idle but parked — behind a permission prompt, a CLI question, an install gate — fails that test, and the refusal is on the group’s audit log as rd-reuse-declined, naming the pane and why.
A hand-back hands the brief to the pane that is already on the session rather than opening a second one beside it, whatever that pane is doing. Where the pane is working, or failed the readiness test above, the driver types the brief into it anyway: two panes on one session are two agents editing one worktree, and the pane that notices is the one that finds commits it did not write. The brief is queued, not an interrupt — the pane reads it when its current turn ends, the way any prompt you send a working delegate does. A pane running a different block is still not taken over, because that would be handing the fix to a different persona on a different model; a session in that state gets a new pane, as it did before. So does one whose pane is so far behind that the delivery itself is refused — a queue already holding its maximum — which is the one case left where a hand-back can put a second pane on a live session; it is on the audit log as rd-takeover-declined, naming the pane and why, rather than looking like a session that had no pane at all. The rd-handback row says which happened in its pane field: reused, taken-over or spawned.
A reviewer is asked again in its own conversation, not replaced by a stranger. Every round after the first is a delta brief — “your previous verdict was fail; here is what changed” — and that only means anything said to the reviewer that recorded the verdict. So the driver reopens that reviewer’s session, whether or not that session was known at the moment the pane was spawned: some CLIs hand orrerix a session id up front and some only mint one after they boot, and before this the second kind got a brand-new reviewer every round, told about a verdict it had never given. Where a session genuinely cannot be reopened the driver opens a fresh reviewer, as it always did, and the audit log says so (rd-lane-resume-failed) with what refused.
Every reviewer brief names the round’s scope on one line. scope: whole-diff, scope: delta since <commit>, or scope: body-only — so a repo’s reviewer persona can key how wide the round measures off something machine-readable instead of counting rounds. The first time a lane is briefed — round 1, or a fresh lane joining later — the scope is the whole diff; a re-brief at a new commit names the commit it is a delta from; a re-brief at an unchanged commit (a body-only fix) is body-only, a verification round over the body as it stands. The scope is per lane, not per round: a reviewer that joins late measures the whole diff, because it has never seen the PR before. The audit row for the spawn (rd-lane-spawned) carries that line verbatim, scope: prefix included, so a tool counting whole-diff rounds filters on the full line and sums distinct (pr, block, round) triples — a round whose reviewer pane is replaced writes one row per open, not one per round.
One reviewer per round, never two. If a PR’s body changes while its reviewer is still writing, the driver waits for that reviewer and hands it the update, rather than starting a second one beside it — two panes reviewing one round is two reviews paid for and one verdict slot to put them in. The refusal is on the audit log as rd-lane-duplicate-refused, naming the pane that already has the round. “Still writing” means the pane is actually working. A reviewer that has finished its turn is idle, and an idle pane is one the driver would rather type into than replace — so if it turns out not to be typeable-into (parked behind a prompt, or with something already queued for it), its session is reopened in a fresh pane instead. Before this those two rules could deadlock on one pane: too unsettled to type into, too alive to replace, which is what happens on every round whose fix was to the PR body alone, because the commit never moves. A new commit is a different matter again: there the reviewer in flight is reading code that no longer exists, so a successor is opened for the new revision exactly as before.
Driving the same PR again keeps the reviewers you already paid for. When a drive ends and you start another one on the same PR — the usual shape: the gate is satisfied, you decide what to do with the findings, then you drive again at the new head — each reviewer’s conversation is carried over, so it comes back to a PR it has already read instead of starting cold — and it is asked again in its own words: “your previous verdict was pass at that commit; here is what changed since”. Nothing the previous drive concluded counts toward the new one; every lane still has to answer at the new head before the gate is satisfied. A reviewer whose session can no longer be reopened simply starts fresh, as it always did.
A reviewer’s pane going away is noticed, not waited out. If a lane’s pane is killed — by you, by the idle reaper, or because the CLI exited — the driver sees it on the next tick and reopens that reviewer’s session in a fresh pane, rather than sitting there waiting for a verdict that can no longer arrive. The audit log records it as rd-lane-reopened, naming the pane that went — and who ended it, where orrerix knows (a pane that exited on its own says nothing about who). A pane the driver released itself is never logged this way: that is rd-lane-released, and the difference is whether the drive lost a reviewer or was finished with one. This matters most right after a cap-refused notice, which asks you to free a slot by killing an idle delegate: a lane that has finished its turn looks idle, and before this killing one cost the drive a full hour of silence. A lane whose panes keep dying still parks on held(lane-stalled) at the usual timeout, measured from when the lane was first asked — replacing a pane does not buy it another hour.
Drives and the merge queue do not overlap, and the exclusion is deliberately not symmetric. A PR with a LIVE drive cannot be queued, and a PR with a queue entry that has not finished cannot be driven; each refusal names the other holder. The asymmetry is the held case: a parked drive moves nothing and cannot race a batch, so it does not block queuing — but resuming it under a live queue entry is refused, so the two loops still never run on one PR at once. The intended order is serial: let the drive reach a satisfied gate, decide what to do with the findings — that decision stays the orchestrator’s, and the driver never makes it — and then queue.
If orrerix cannot read its own drive record — a torn write, or a file from a newer build — it says so, loudly, and stops driving that group rather than guessing. It never repairs the file and never deletes it. “Nothing is being driven” and “orrerix can’t tell what is being driven” are the same picture otherwise, and only one of them means your PR is fine.
With no driver: block none of this exists and nothing about your group changes.
Guardrails
Enforced by orrerix, not the model:
- a cap on live agents (≤12, set at launch and adjustable live);
- models pinned per role at launch;
- the permission mode fixed at group creation (native auto mode or acceptEdits — never bypass).
Who can close a PR
gh pr close goes through the same shim as gh pr merge, and it is decided before it reaches GitHub. A worker, reviewer or planner may close only a PR whose head branch is its own — or a scratch branch beneath it, under a / or - (so fix/2985-x covers fix/2985-x-scratch2, and a bare prefix like fix/29 covers nothing). The orchestrator may close any PR in its group. Anything else is refused, with a message naming the PR, the branch it belongs to, the agent that owns it — or, when no agent on the group’s roster does, that nobody does — and what the calling agent does own. A worker resumed onto an earlier session — including every review-driver hand-back — keeps that session’s branch, so it can still close its own scratch PRs.
This exists because of a live incident: a worker cleaning up its own scratch PRs looped over PR numbers it had built by string concatenation, and closed five other workers’ open PRs in six seconds. Every review drive on them was cancelled, and because gh runs under your own token, the GitHub timeline could not tell an agent’s close from yours.
Two more things follow from that. Every close — allowed or refused — is now an audit row carrying the calling agent’s id, so the next orchestrator reads the audit log instead of asking you whether the closes were deliberate; and gh pr reopen is never refused (it destroys nothing, and it is the remediation) but is audited the same way. Your own closes from the GitHub UI are untouched: the shim only ever sits on an agent pane’s PATH.
gh pr merge --delete-branch is deliberately outside this — it is the branch cleanup that belongs with a merge, and it already sits behind the merge gate.
Compact-nudge
The orchestrator pane lives for the whole session and every turn re-reads its entire history — it’s typically the biggest token consumer in a group. Orrerix can drive the CLI’s own /compact for it at a natural lull: once an eligible pane has been idle at its input prompt (the same output-quiet signal the watchdog and idle-tick already read — never mid-turn) past a configured window, orrerix pastes /compact for it exactly like any other prompt delivery — no PTY resize, no new agent capability — and it never overwrites text you’re mid-typing (a held nudge is silently skipped, not queued; it just tries again at the next natural lull).
Configure these live in the group’s lifecycle panel (Alt+O), alongside its other guardrails. The quiet-window (minutes) is off at 0; optionally choose eligible roles — the orchestrator only by default, since workers are short-lived and rarely worth compacting. The nudge fires for every CLI that has a compact command orrerix can paste — claude, copilot, codex, pi and opencode, each getting its own. Gemini has none (its equivalent is /compress), so a gemini pane is skipped, and request_compact from one says why.
The timed nudge also checks context is actually full before it fires — a smart default, no setup needed. Quiet is not the same signal as full: on its own, a quiet-window nudge fires at the right moment but the wrong context level, compacting a pane at 20-30% full and paying a whole re-grounding cycle for one that wasn’t running out of room. So once you enable the quiet-window (above), a minimum context floor is on automatically at a sensible default (50%) — nothing to configure. Three states, if you do want to tune it: leave it alone (the 50% default applies as soon as the quiet-window is set); set it explicitly to a percentage of your own choosing; or set it to 0 to go back to firing on the quiet window alone, with no context check at all. This floor only ever governs orrerix’s own unprompted timing — calling request_compact() yourself always fires immediately, at any context level, because that’s your judgment call, not orrerix’s.
codex, pi and opencode panes with no known context window are exempt from the timed nudge while the floor is on. The floor needs a percentage, and a percentage needs the pane’s window. opencode never records one. codex and pi record one only once the CLI has reported it. Without it orrerix cannot tell a 10%-full pane from a 90%-full one. It skips the timed nudge for such a pane rather than pay a whole re-grounding cycle at an unknown fill level. All three CLIs compact themselves when they run out of room anyway. Set a context-window override on the group to give those panes a window, and the floor then applies to them normally — while context escalation is on. With the escalation threshold at 0, orrerix computes no context percent for any pane, so the floor has nothing to measure: it still refuses a pane with no window, but lets a windowed one through at any fill level, a Claude pane included. Setting the floor to 0 also brings them back, on the quiet window alone. request_compact() works for them either way.
Context escalation is enabled at 45% by default for new groups. The lifecycle panel lets you change the threshold from 0 (off) through 100%; at the threshold, loomux delivers a context notice and asks for compaction at the next check if the agent has not asked. Escalation is restricted to the group’s configured compaction-eligible roles; by default that is the orchestrator only, so workers and reviewers are not auto-compacted by this fallback. Existing saved groups retain an explicit 0 choice. For an existing group with a nonzero threshold, delegates that previously received threshold escalation now need their role included in compact_nudge_roles to continue doing so; that role list has a backend setter but no lifecycle-panel control. The same panel exposes the quiet-nudge minimum context percentage (0 disables that floor; an untouched value uses the 50% smart default).
The orchestrator can also ask for it directly. request_compact() is the primary mechanism — the timed nudge above is the fallback for personas that never call it. The orchestrator (or any agent) calls it as the LAST action of a turn, at a natural lull; orrerix pastes /compact the moment the pane actually goes idle, not immediately (a mid-turn write would land as a queued message). Before calling it, the persona is expected to offload durable state (task board, set_state, relevant GitHub issues/PRs) — the tool warns, but never blocks, if that looks skipped. If a group sets a context-usage threshold (percent of the model’s context window), crossing it delivers an [orrerix] context at NN% … notice; if the agent still hasn’t asked by the next check, orrerix requests one on its behalf rather than letting the CLI hit its own emergency auto-compact with no offload.
Orrerix also catches that emergency auto-compact itself, when it happens anyway. There’s no way to plan around a compact nobody asked for, but orrerix recognizes Claude Code’s own auto-compact banner in the pane and treats it the same as any other compact: whichever way one gets triggered — the timed nudge, a direct request, the threshold fallback, a human typing /compact by hand, or the CLI’s own emergency auto-compact — once it’s done, orrerix re-grounds the pane in its full role instructions (not just a pointer to go re-read them) and prompts it to re-sync live state. Before doing so, orrerix checks that context actually shrank (a real signal a compaction ran, not just an ordinary quiet moment) — if it can’t confirm that, it skips the re-grounding rather than risk delivering it on a loop.
How orrerix knows a compaction finished depends on the CLI. Where the CLI says so itself, orrerix takes its word over any guess:
- claude — orrerix’s per-pane
--settingsfile adds aPostCompacthook beside itsPreCompact,SessionStartandUserPromptSubmitones. When it fires, the re-grounding follows without waiting for the pane to go quiet or for context to shrink. Claude’s ownSessionStarthook re-grounds a compacted session natively; orrerix waits one check — about ten seconds after it notices thePostCompactmarker — for that one first, and does not send a second copy when it arrives within that wait. If it arrives later — a slow hook on a busy machine — orrerix has already sent its own, and the pane gets both. ThePostCompacthook is only added for Claude Code 2.1.76 or later, the first release that has it; on an older or not-yet-detected version orrerix relies on the other signals. - pi and codex — the session file records the compaction: pi writes a
compactionentry, codex acompactedrecord. orrerix reads those; it installs no hook. codex runs a hook only after you trust its exact definition in its/hooksbrowser, and orrerix never records that trust for you. - copilot — its own on-screen “Compaction completed” message; failing that, the pane going quiet after its
preCompacthook fired. - opencode — it records no compaction signal orrerix can read, so orrerix relies on the pane’s output and context, as above.
Directive ledger. Any agent can call note_directive(text) to jot down a one-line diary entry — a human directive, a scope decision, a piece of feedback — the moment it receives one, before acting on it. The point is timing: an emergency auto-compact strikes with no warning turn, so there’s no “offload before it happens” moment to rely on for something that only ever lived in the conversation. Orrerix embeds each agent’s own ledger (its recent tail, size-capped, pointing at the full file if anything had to be cut) right alongside the role instructions in that same re-grounding notice, so a directive survives a compact even when nothing warned anyone first. note_directive(text, replace: true) rewrites the whole ledger in one shot — how an agent curates it after being shown its own tail, dropping anything already done or no longer relevant. The ledger lives at <data dir>/orrerix/orchestration/<group>/ledger-<agent-id>.log — a plain, human-readable file, one entry per line, that a human can open directly.
Lifecycle panel. The group lifecycle panel (Alt+O) shows each agent’s detected model, effort and context-window usage beside its uptime and cost. It uses the CLI-reported window when available; usage percentages are shown only when both tokens and a window are known. The model-name fallback below is Claude’s, so it only ever fills in for a Claude pane: a codex, pi or opencode pane whose CLI has not reported a window shows its tokens without a percent, and the context threshold does not escalate it — the panel and the escalation read one rule, so the panel never shows a percent that could not escalate, or the reverse. A context-window override on the group supplies the window for any CLI. Before a reading exists, the declared model and effort are labelled as declared. The effort shown is always one the CLI reported: a pi pane whose session no longer names its thinking level shows no effort rather than passing its configured level off as the live one. The panel shows the compact-nudge phase only when there is something worth a look: armed (waiting to observe the pane go busy), awaiting evidence (busy observed, waiting on quiet to resolve), re-grounding (a reinjection is in flight, with its attempt count), a recently finished re-grounding (see below), or a recent lost outcome (an arm or delivery that did not resolve in time and was released rather than left stuck). An idle agent with nothing pending shows neither line. The percent is computed against the context window Claude Code itself reports, so a 1M-token session reads correctly on any model. Until a session’s first report arrives, orrerix falls back to a conservative guess from the model name. A group can override the window explicitly if the report is ever wrong for a given deployment. If a pane ever holds more tokens than the chosen window, orrerix widens the window to match rather than showing an impossible figure. An explicit override is never widened.
Your status line still shows in orrerix panes. Claude Code’s reported window arrives through its status line. orrerix gives each Claude pane its own statusLine setting, which records the reading and then runs your status-line command with the same input, so the line you see is yours. orrerix reads your command once, when the pane starts, from the first of .claude/settings.local.json and .claude/settings.json in the pane’s directory, then ~/.claude/settings.json, and keeps its padding and refreshInterval. If you have no status line configured, the pane shows an empty one. Claude Code also hides most of its footer keyboard hints (esc to interrupt, ? for shortcuts) whenever any status line is set, and orrerix needs one for the window report, so those hints are hidden in orrerix Claude panes. Three more exceptions:
- A status line set in managed (organization) settings takes precedence over orrerix’s. Your pane shows that line, and orrerix falls back to its model-name guess.
- A status-line command edited after a pane started takes effect in that pane only once it is relaunched.
- A command that prints and then exits non-zero still shows its output in an orrerix pane, where plain Claude Code would blank the line.
A finished re-grounding tells you how strong the evidence behind it was. Orrerix stops retrying a re-grounding on one of two signals, and they are not the same strength, so the panel says which one it got rather than reporting both as success:
re-grounding delivered— orrerix’s own submit sampler watched the notice’s Enter land. The text reached the pane’s input box and was submitted.re-grounding unproven (agent alive)— no delivery confirmation ever arrived, but the agent called an orrerix tool afterwards. That proves the agent is alive and working; it proves nothing about the notice. A re-grounding that was genuinely lost, on a pane that happened to be busy for its own reasons, finishes exactly this way.
Neither one proves the agent read the re-grounding — nothing orrerix can observe from outside an agent’s session does, so it doesn’t claim to. The audit log draws the same distinction, under two separate actions (compact-reinjection-confirmed and compact-reinjection-liveness-only), so counting one of them doesn’t quietly include the other. The safety net underneath both is unchanged: a re-grounding that neither confirms nor draws any sign of life gets bounded retries and then a visible lost-outcome record.
Cache age: how long a pane has been quiet
Every agent pane’s header wears a small chip that says how long ago the pane last made a model request, measured against its provider’s prompt-cache TTL:
hot 3m: the last request was recent, so the next one should read a warm cache.cooling 48m/60m: the pane is within the last stretch before the TTL runs out. Compacting now is cheap; waiting is not.cold: the TTL has passed. The next request re-reads the whole context uncached.idle 12m: the pane’s CLI has no known TTL (copilot, opencode and pi route to several providers), so you see the age and no claim about the cache.cache —: the pane has no reading yet. The tooltip says what is missing (below).
The state is inferred, not observed. Nothing tells orrerix that a cache is still there. It knows when the pane’s usage counters last moved, and it knows the TTL the provider documents. The inference leans slightly toward “hot”, by about the length of the last response, and the cooling stretch is sized to cover that. The chip rides the same snapshot read the tab strip already makes, so it costs no extra polling.
Which panes wear the chip. Every pane that runs an agent CLI: a solo pane you launch from the launcher, a lead pane, and each orchestration agent. A launcher pane gets an orrerix identity as it opens, so its usage can be read. claude, pi, copilot and codex get the full identity when channel tools are on. Any other agent pane is adopted as a delivery-only member, and nothing is added to its command line. A terminal pane has no chip at all. A pane restored after a restart resumes its session, so its chip comes back with the age that session had on record, usually cold. A session with nothing on record reads cache — until its next request.
cache — is the muted chip for a pane the chip cannot read. Its tooltip names the missing piece:
- Not registered: orrerix has no identity for the pane, so it cannot find its usage.
- Copilot writes no token record, so orrerix cannot see its requests.
- An SSH pane runs its session on the remote host, which orrerix cannot read.
- No session yet: codex and opencode name their session only after they start, so the chip waits for it.
- No request yet: the session is known, but its first request has not landed.
Click the chip for three things:
- What the next prompt will cost, three ways. See below.
- What the last wake cost. The first request after the last quiet stretch, split into tokens read from the cache, tokens written to it, and uncached input (plus a dollar estimate where one is known). A cold wake is mostly written; a warm one is mostly read.
- Compact now. orrerix types
/compactinto the pane at its next idle moment, never mid-turn. It uses the same path an agent’s ownrequest_compact()uses, so the pane is re-grounded afterwards like any other compact. The item is greyed out, with the reason, on a pane it cannot compact: a CLI without/compact, or a pane that is not an orchestration agent. If the compact cannot fire right away, the confirmation says “queued” and names everything holding it: the group is paused, a compact is already running on that pane, or the group has used its compacts for the hour. It fires once every one of those has cleared.
The Agents tab shows the same label on each agent’s row.
TTLs. orrerix takes the first of these that answers:
- The block’s own
cache_ttl_minutes:in.orrerix/workflow.yml, if you declared one. - What the pane’s session shows. Claude Code records each cache write as a 5-minute or a 1-hour write, so a claude pane on the 1-hour cache reads as 60 minutes with nothing declared. That is what covers a pane with no workflow block at all. The session’s most recent cache write decides; if one request wrote to both, the shorter one counts.
- The CLI’s default: 5 minutes for claude and codex. That is the shortest lifetime their providers document, chosen so the chip never says “hot” over a cache that is gone.
The chip’s menu says which of the three it used.
The orchestrator compacts before it goes idle. The orchestrator is told to compact before ending a turn with nothing in flight: no delegates, drives or watches. That way the next wake reads a small context instead of the whole session cold. This does not contradict its other rule, “don’t compact below 50% context without a specific reason”: ending a turn with nothing in flight is listed as one of those reasons. If it forgets, orrerix nudges it once, typing [orrerix] going idle with no work — compact now into its pane, when all of these hold:
- it has been quiet into the cooling stretch, and its cache is not yet cold;
- nothing is in flight: no live worker, reviewer or planner, no pending watch, no pending intake, no queued delivery, and no live review or plan drive;
- its context is at or above the compact floor (50 % unless the group set one).
The nudge is sent once per idle stretch. If the orchestrator reads it and decides not to compact, it is not nagged again until work goes in flight or a compact lands. Each nudge is written to the audit log as cache-idle-nudge. A live watch always counts as in flight, because orrerix knows when a watch expires but never when its CI will finish.
The design argument, the residuals and the tests are in docs/design/cache-age.md.
What the next prompt will cost
A pane with a large context is cheap to keep talking to while its cache is warm and expensive once it is not. The chip’s menu puts a figure on that before you send anything, so that “send now, or start a new agent” is a comparison of numbers:
- Send now: the cost with the cache as the chip reads it. While it is
hotorcooling, the pane’s context is read from the cache and your prompt is written to it. - Once it is cold (or, on a pane that is already cold, Had it stayed warm): the other state, for comparison. Cold means the whole context is written to the cache again.
- Same prompt in a fresh agent: what the same prompt would cost in a new pane. It is measured from this session’s own first turn: the CLI’s system prompt, its tools, your repo’s instruction files, and that session’s first prompt.
- Inputs: the context size, the typed prompt, the model and price date, and which cache-write rate was used.
Hover any row for what it assumes.
On an orchestrator pane the same estimate also sits on one line under the compose strip, and moves as you type. On every other pane orrerix cannot see what you type into the CLI’s own input box, so the figures are for the history alone and the menu says “typed prompt not visible”.
What the figures are, and are not:
- Tokens first, dollars second. Tokens are exact for every account. Dollars are the vendor’s list price, with the model and the date the price was read. A subscription pays no per-token price, so on one the dollars only show the relative size of the three options.
- An estimate of the input side, for one request. The reply’s output is priced on top and is not included. A turn that makes N tool calls reads the cache about N times.
- The cache state is inferred. “Send now” is the warm figure only if the cache really is still there. The last wake, shown above it in the same menu, is what a real request on this pane read and wrote.
- Your typed prompt is counted by its length, not tokenized, so it is rough. It is normally a tiny part of the figure.
- No price, no dollars. Only Claude models have a list price in orrerix’s table. A codex, pi or opencode pane shows tokens and no dollar figure. So does a Claude model the table does not know; a newer version of a known family is priced at that family’s highest current price and says so.
- No reading, no figure. A pane whose CLI records no context size shows “No context reading for this pane yet” instead of a number.
- No cache state, no “now”. Where orrerix cannot say whether the cache is warm, you get all three figures anyway, cold first, under a row that says the state is unknown and why. That is a pane whose chip reads
cache —because no request has been seen on it yet, and a pane whose CLI has no known cache lifetime. Clicking acache —chip opens the estimate when the pane’s context is known, and its tooltip says so. It opens nothing when the context is not known. - Attached images are not counted. The estimate covers the text in the compose strip. Images you attach are sent as file paths for the agent to read, and neither those lines nor the cost of reading them is in the figures. The line and the menu say so when images are queued.
The formula, the price table and its source, and how far the figures were checked against real requests are in docs/design/prompt-cost.md.
Persistence & restart
Each group keeps durable state under <data dir>/orrerix/orchestration/<group>/:
state.json— the orchestrator’s queue/plan memory (written via a tool after every change);audit.jsonl— every tool call, prompt, spawn, and exit, one JSON line each;agents.json— the roster (which sessions belonged to which role);- the rendered role instructions, plus the rendered orchestrator playbook (
orchestrator-playbook.md) — the on-demand half of the orchestrator’s contract, which the orchestrator reads one section at a time withread_playbook(section)(#1683); ledger-<agent-id>.log— each agent’s own directive ledger (see Compact-nudge above).
The group id is derived from the repo path, so relaunching an orchestrator on the same repo resumes its state; GitHub issues remain the source of truth for the work queue.
Restart after orrerix closes: open the session browser (Ctrl+Shift+P). Its Orchestrations section, at the top, lists every group orrerix has a record of — on every agent CLI — and Resume there restores the whole orchestration: same group id, state, task board, and audit history, with fresh MCP identity wired into the resumed conversation. A plain claude --resume / copilot --resume (or opencode’s --session <id>) would come back powerless (no MCP tools, no task board); this path never does.
That section reads orrerix’s own record of the group (group.json plus the orchestrator row of agents.json), not any CLI’s session store — which is why it is the route to use. The session list below it is a scan of each CLI’s own store, and an orchestration group’s OpenCode sessions are deliberately not in it: they live in the group’s own store, never your global one (see Session browser). Orchestration sessions that are in that list still carry ORCH / W / REV chips and still restore their group when clicked — the route is decided by recorded membership, never by which CLI wrote the session — and a worker/reviewer row rejoins its group once the group is running.
A row that cannot be resumed says why, instead of offering a button that fails:
| What the row says | What happened | What to do |
|---|---|---|
| Running now | The group has live agents in this window | Click Focus on the row — it brings that group’s orchestrator pane back into view, out of the dock or out from behind a fullscreen pane, in whichever project tab holds it |
| Session not yet identified | Copilot and OpenCode mint their session ids after boot, and orrerix has not learned this one yet (or its watcher timed out) | Wait for it. If the watcher timed out there is nothing to resume by hand — start a fresh orchestrator, which reattaches to this group’s existing board and roster |
| Recorded session is no longer in the … store | The CLI’s own history no longer holds that conversation | Start a fresh orchestrator — it reattaches to this group’s existing board and roster |
| This group’s record could not be read | The group’s group.json is missing or damaged | Repair or remove that file; nothing can be resumed safely until orrerix can tell which CLI ran it |
Per-task sessions: each worker is scoped to exactly one work item, and orrerix records its session id. Follow-ups on a finished task resume that worker’s session (same context, same workspace) instead of cold-starting a new agent or disturbing a busy one.
The delivery queue (above) persists to disk. A restart doesn’t drop a queued prompt: an entry addressed to the group’s orchestrator, or carrying the same CLI session id as a pane that comes back, redelivers automatically in its original order. Everything else is surfaced rather than replayed — the orchestrator’s session-start re-sync lists it with the payload intact where one was recorded, so it can re-derive and re-send what still applies, rather than the prompt silently vanishing. The one true loss is a delivery caught mid-submit when orrerix went down: that text is not recoverable, and the recovery notice says so plainly. docs/design/orchestration.md’s “Delivery queue (#445)” section carries the full design.
Autonomous mode
Everything above describes the supervised default: the orchestrator advances work in response to your nudges (▶ Start, the label handshake, steering) and your merge-gate decisions, and no agent ever merges or publishes.
Two opt-in modes go further — an autonomous mode where the orchestrator wakes itself on an idle timer and pulls labeled work while you’re away (under a token budget and optional auto-merge / auto-release consent toggles), and a supervised dangerous mode that lets agents merge and release without per-item approval while you’re present. The default-branch merge/release gate that backs them is structurally enforced, not just asked of the model.
→ See Autonomous & supervised modes for the full picture: the idle tick, the cost/budget money-stop, each consent toggle and what it gates, the per-item approve-with-comment grants, and the gate’s audit trail.
Requirements
- An agent CLI on
PATH—claude,copilot,gemini,opencode,pi, orcodex. Roles can run on different ones (see cross-model reviewers). The launcher warns inline as you pick, and re-checks on submit — if one of those CLIs isn’t onPATHit refuses the whole launch rather than starting the group. A CLI named by a.orrerix/workflow.ymlblock is not checked at all, and shows up instead as a pane that opens and immediately exits with the shell’s not-recognized error. ghCLI authenticated for the issue/PR/review workflow.