Skip to content

Orgabot is in private alpha. Request access, or join our Discord.

← All documentation
Using Orgabot

Missions in depth: routing, workers, and recovery

Mission variants, routing and decomposition, the worker that runs them, observation and control commands, and the recovery paths when one dies.

Last updated

Variants

VariantCommandWhat it means
Solo missionorgabot mission <project> "<instruction>"Runs on your own credentials, delivers through your ambient gh. Force with --no-org.
Org-routed missionthe same command, on an org-mapped projectAttributed to a role; delivers with that role's scoped GitHub App token. Decomposes by default.
Pinned roleorgabot organization mission <org> <role> <project> "…"Runs as a named role, skipping routing.
Fan-outorgabot mission-all "…" --only a,b,c or --all [--exclude x,y]One instruction across many projects.

Routing and decomposition

An org mission goes through one AI step that either picks a single role or produces a multi-role plan. --no-decompose forces single-role routing; --decompose is a back-compatible force-delegation hint.

An org-owned mission is owned by a ROLE. The generic, unattributed Orgabot worker is the sanctioned owner only for a project no organization maps, or when you pass --no-org. If an org maps the project and no role can be auto-selected (routing resolved nothing, or the role-owned launch failed), the mission holds and asks you to pick a role. It must never silently fall back to a generic worker.

Routing derives ownership from the requested objective and the capabilities the work needs. Testing requirements, review-loop instructions, and constraint preambles do not establish QA or oversight ownership. When AI routing is unavailable, the fallback selects a role only if declared capabilities identify one qualified owner; it never selects from prose keywords or a default seat. Missing evidence, multiple qualified owners, or a low-confidence AI assignment holds the mission and asks you to choose. You can also pin a role explicitly with the command above.

Workers

WorkerSelected withNotes
Claude Code adapterdefaultDrives the claude CLI. Brittle stream parsing is isolated in a pure, unit-tested interpreter.
Codex adapter--worker codexA ChatGPT-subscription Codex session.
API-key executorsrole configurationNative streaming, acknowledged cancellation, honest stateless session continuity.
Mock worker--mockDeterministic, model-free canned edits for reproducible tests and demos.

Which engine and model powers a role is configuration, not a code change:

orgabot organization role-provider <org> <role> <claude|codex|default> [--model m]
orgabot organization default-worker set <org> <engine>
orgabot organization role-fallback set <org> <role> "claude:opus,codex"

A worker with shell is told not to run the suite Orgabot is about to run. A shell-enabled worker can run anything its live role grants permit, and workers used to spend the last stretch of Implement running the project's build and full test suite, then report the result in prose. Orgabot does not accept prose as verification, so it ran the identical suite itself the moment the worker finished, and the mission paid for it twice. When the mission's post_implement gate is a full local verification Orgabot owns, the brief now says so and asks the worker to use cheaper checks instead. Only the test commands are off limits: the project's own build or typecheck stays available, because for a TypeScript project the build is the typecheck and banning it would ban the check the same sentence recommends. Targeted tests of the files the worker changed stay available too. Nothing about the gate changes. Orgabot still runs the full suite on that exact commit, it still holds the mission when the suite is red, and worker output is still never evidence. If the gate does come back red, that repair round is briefed normally and running the failing command is exactly the right move. A round that follows a reachability rejection or a review finding has no failure to reproduce, so it is decided the same way against the gate that runs next for it, post_repair. Where that resolves to a light profile, CI, or nothing, there is no full local suite coming and the ordinary brief stands. The brief is left alone when the worker has no shell, when the turn delivers something other than a diff, or when the gate is CI or a light profile, since in those cases Orgabot is not the one about to run it. It is also left alone for a mission that exists because something is already red: a recovery sweep, an open-PR resumption, an incident fix, a CI-remediation round, any mission fixing an issue Orgabot filed for a red CI run (recognized by the issue's own ci-failure label, so every route onto such an issue is covered, including a retry), and a continuation of a mission that held on a red suite. Those are the repair case one step earlier, so they keep the ordinary brief and the worker is free to reproduce the failure. The brief is now the only place that says anything about verification: the instruction builders for issue fix-and-ship, recovery, CI remediation, and CI-failure triage each used to append the same "run the build and test suite until both pass" line, which only the brief has the mission's resolved policy to decide. Their constraint about never weakening a test to make it pass is unchanged.

A network or lock failure is not a red suite, so it never gets a repair round. When the failing verify command's own closing error names a DNS or connection failure (getaddrinfo ENOTFOUND fonts.googleapis.com, EAI_AGAIN, ECONNRESET, a fetch timeout) or one of Orgabot's own busy state locks (could not acquire verify-slots lock; retry shortly), nothing judged the change. Orgabot re-runs the same verification on the same commit, backing off from 15 seconds up to 2 minutes between runs for up to 15 minutes, and only a real result moves the mission on. The check is deliberately narrow: if a test runner reported anything at all (a FAIL row, an assertion, a failed-test count), the failure is treated as real even when the output also mentions one of those codes, and a refused connection to localhost counts as the project's own setup, not an outage. If the outage outlasts the 15 minutes, the mission is held as waiting on verification_unavailable (error class infra) with a summary saying verification could not produce a result. That is not a red gate and nothing is repaired, and you do not have to clear it: the recovery sweep (the headless supervisor or orgabot recover sweep, and orgabot mission resume-due) starts another round on the same branch that changes nothing and re-verifies the same head. It waits 5 minutes before the first of these rounds and doubles the wait each time up to an hour, and the schedule is stored on the mission so a restart does not reset it. After 10 rounds that still meet an outage, the hold becomes verification_unavailable_exhausted, which is yours: resume the mission once the network is back. A verification the machine slept through is held the same way (on verification_unavailable, error class host_sleep) and resumed by the same sweep after the wake, so a laptop closed mid-verify no longer leaves a mission parked until you resume it. A red gate is never re-verified this way; it keeps its own repair path. A busy verify-slot lock is also retried while Orgabot waits for a slot, so it no longer fails a mission or makes a worker that finished its work look dead.

A repair round that changes nothing is an answer, not a contradiction. If a repair worker finds nothing to fix, Orgabot verifies the same commit again instead of reading the empty diff as a result, and that run's outcome stands. A continuation round on an existing branch that makes no new edits is also measured against its base before it is called "no changes": Orgabot fetches a base branch the checkout is missing, and goes on to verify the commits the branch already carries. If the base still cannot be read, the mission is held on the same verification_unavailable hold, which the recovery sweep retries the same way, never reported as a worker whose account contradicts an empty branch.

The fallback chain is for unavailability only: an exhausted or unreachable pool. A healthy worker is never abandoned mid-run because a round took a while, and a stale belief about a pool must reorder the chain, never delete an available executor from it.

A worker that never starts is an unavailable rung. Every launched worker, including a follow-up that resumes a recorded Claude session, has a startup window (5 minutes by default, --startup-timeout <minutes> to change it) to stream its first turn. Launch chatter does not count: the Claude session announcement and API retry notices are what a Claude Code launch on an exhausted account prints before it sits silent without ever erroring. A worker that streams no turn in the window is stopped the same way the idle watchdog stops one (asked to exit, then forced), and the round continues on the next executor in the chain (for example Codex, then Grok) in the same worktree, on the same branch and plan node, with the usual handoff. A session recorded by the rung that never answered is not resumed on the rung that replaces it. With no rung left, the round fails and says the executor never started. It does not mark the pool exhausted, so other missions keep using it.

A worker waiting for a host slot has not started. Orgabot admits a bounded number of worker sessions at once across the machine (orgabot verify slots shows them). A round queued behind that budget launches nothing until a slot frees up, and its log and long-run notice now say it is waiting for a host worker slot rather than "starting up". orgabot stop on such a round is acknowledged as soon as it is read: the worker will not start, and the round ends with the branch and worktree preserved.

A failover is a handoff, not a restart. When a rung dies mid-round on a usage limit, a login requirement, or a structural refusal, the next rung continues in the same worktree on the same branch, and its brief now carries an executor-handoff section: which executor left and why, the paths that executor left uncommitted in the worktree (or a plain statement that the tree could not be read), and that executor's last message before it was cut off. The section is labeled as worker output, so the next executor treats it as data about where the previous one was and never as instructions. It is told to read git status and git diff before editing and to continue from the existing work rather than start over or discard changes it has not read. Every later worker run on that mission, including verification and review repair rounds, sees the same handoff, and a second failover appends a second entry.

Observing and controlling

orgabot status                 # everything in flight
orgabot logs <mission-id>
orgabot mission show <mission-id>   # the mission, who spawned it, and what it spawned; --json
orgabot wait <mission-id>      # blocks; --json
orgabot cancel <mission-id>    # stops a running mission and marks it cancelled
orgabot inputs                 # mid-mission questions waiting on you
orgabot answer <id> "<answer>"
orgabot objective list|show|check

orgabot inputs only lists. Given anything after it (orgabot inputs <id> accept), it answers nothing, says so, and exits 2 with the orgabot answer <id> "<value>" form to run instead; it never echoes the value you typed, in case it was a secret. An unrecognized command, or a known one missing its subcommand, says which and exits 2.

Mission records and logs live under ~/.orgabot/missions/.

Which mission spawned which. A mission launched by another (a retry, an objective fan-out, a CI-failure fix, an open-PR resumption, a follow-up) records its parent once, at launch, and that record can never be rewritten. orgabot mission show <mission-id> prints spawned by <parent-id> (<relation>) on the child, followed by the parent's status and title, and a spawned: list with each child's status, relation, and title on the parent; orgabot timeline <mission-id> --lineage prints the whole chain from the root down with its correlation id, and adding --compliance lists every generation's gate decisions. Where the dashboard's terminal chip names the run a CI fix was launched for or the pull request a resumption resumed, the CLI names it too: (ci_fix, run 4242), (resumption, PR #77). A parent whose record is gone shows as [parent record missing], never as no parent; a recorded parent edge that cannot be read shows as spawned by unknown, never as a root; and if the mission index cannot be read the children show as unavailable rather than none.

A question finds you; you do not have to go looking. When a worker asks something mid-mission, the mission host announces it within seconds on the mission's own log (orgabot logs <mission-id>) with the exact orgabot answer command, and posts a local notification on macOS (clickable back to the launching iTerm2 session when terminal-notifier is installed). An epic's parent mission logs the same line for a question any of its children raised, so following the parent is enough. Each question is announced once; orgabot status and orgabot inputs keep listing it until you answer.

Every mission writes that log, including one you run in the foreground. A foreground orgabot mission still streams to your terminal exactly as it did; it now also tees the same lines into the mission's log, so orgabot logs <id> and the dashboard's mission terminal show the run's output rows beside its lifecycle events, whichever way you started it. Output is redacted line by line before it is written, and again when it is read, so a secret a worker prints does not land in the log. A run that is already writing that file (a --background mission's detached child) is not tee'd a second time, so no line is ever recorded twice.

Mid-mission questions

A worker that needs a decision only you can make asks one (orgabot inputs lists them). While a question is open the mission is suspended on it, and answering continues the round from exactly where it paused.

A question comes to you; you do not have to go looking for it. The same open question appears on every surface, whatever launched the mission: orgabot status lists it in its own Open questions for you section with the question text and the command that answers it, orgabot wait prints it the moment it is raised, and the dashboard inbox shows it. A wait on an epic (or any decomposed objective) watches the whole family: a question raised by a CHILD reaches the session watching the parent, and it is what the wait reports, rather than being swallowed by the parent still reading running.

An unanswered question does not stop the mission. A worker is allowed to carry on without an answer, and when the round finishes past a question the request is withdrawn as abandoned; the round keeps its commit, its independent verification, its review, and its pull request. Cancelling a question yourself is different: that is a decision to stop, so the mission is held on input_cancelled.

Walking away does not cost you a question. The wait is bounded (a worker cannot hold a session open for hours), but the question is not. When an asking worker gives up waiting, the question stays open and answerable in orgabot inputs instead of being destroyed, and it now knows what answering it should continue: answer it an hour later and the mission relaunches on its own branch through the ordinary gated path, with your answer carried into the brief as context. A worker may only carry on with a default where its own ask declared one (orgabot ask "…" --default "<value>"), which is a statement by the code that proceeding without you is safe and reversible; only then is the question withdrawn, and the record says which default was used.

A withdrawal says who withdrew it. A question the asker gave up waiting for, or that ended with its mission, records that fact in those words. Only a cancel you actually made is recorded as the operator cancelling it.

`orgabot answer` tells you what it actually resumed. If the answer relaunched a mission it names it. If the mission had already ended, it says so rather than reporting a resume that did not happen; when that mission left verified and reviewed work behind, deliver it with orgabot ship. And if nothing at all was waiting on the request, the answer is recorded, the command says so plainly, and it exits non-zero rather than telling you a mission will continue when none can.

Cancelling a question never overwrites a decision. orgabot answer <id> --cancel checks the request's current state at the moment it acts:

  • A pending question is cancelled, and the mission is held on input_cancelled.
  • A question someone is answering right now (a resume claimed it in the last 10 minutes) is left alone. The command says "a resume is in progress", cancels nothing, and exits 75 so a script can retry. A claim older than 10 minutes belongs to a resume that never finished, and --cancel clears it.
  • A question already answered or cancelled is not rewritten; the command says which, and exits 2.
  • If the inputs store is busy, nothing is written; the command says so and exits 75. Run it again.

The same rules protect the other direction: an answer that arrives after its question was withdrawn (for example because the mission was ended mid-resume) is refused rather than recorded over the withdrawal, and orgabot answer says so. A resume may already have acted by then, so check the mission before acting again; only a review-loop merge re-reads the request (and refuses a withdrawn one) before it merges. And if the store was busy only because someone else was answering or cancelling the same question, you are told it is already answered or cancelled, not asked to retry, and the mission is not re-suspended on a question the other writer already resolved. The dashboard's cancel and answer buttons follow the same rules: a question that is no longer pending is refused (HTTP 409), and a busy store asks you to retry (HTTP 503).

Deciding an approval never overwrites another decision. If you approve in the dashboard while orgabot deny runs on the same approval (or two tabs race), exactly one decision lands, and the approval record always agrees with what the mission actually did. The other caller is told the approval was already decided, by whom, and when: orgabot approve/orgabot deny print that and change nothing, and the dashboard's Approve/Deny buttons (which lock the moment you click) unlock again and tell you who already decided it. If a decision was recorded in the mission's journal but its approval record was never updated (for example because the deciding process died in between), the next decision attempt settles the record to the journal's decision instead of deciding it the other way. If the approval store is busy, nothing is written: the command says so and exits 1, the dashboard puts the buttons back and says the store was busy, and you run it again. Ending a mission while the approval store is busy still ends it; any approval it could not deny stays pending and is listed with the command to deny it (orgabot deny <id>, or orgabot stop <mission> again). Organization approval requests (orgabot organization approve/deny and the dashboard's organization queue) follow the same rule: one decision at a time, a late decision the other way is refused, and a busy store refuses before anything is recorded.

A hold the worker never heard about

A mission can be held for bookkeeping reasons while its worker is still running: the clearest case is a question that was withdrawn (above) on a mission that was mid-round. When that happens and the worker goes on producing real evidence (a verification, a review, a push, a pull request), the hold is stale, and the evidence wins: the round is re-opened and the observation is recorded, so delivered work reaches mission authority instead of being dropped.

This never applies to a hold a human owns. An approval, a capability grant, a policy refusal, an unresolved workflow shape: a worker's own evidence may not lift any of them, and an observation that arrives against one is still refused, now with an alert rather than only a log line, because a refused verification or delivery is the system telling you it just lost a fact. An epic also reports a node that delivered and was then parked as delivered, naming its pull request, rather than counting it as no progress.

Where a question is stored

Questions live in the local operational store (~/.orgabot/operational.db), which answers every read and takes every write; there is no JSON copy and no fallback. If the store cannot answer, the read fails rather than quietly reading a file next to it.

Your typed answer is treated as a credential. The database file and its WAL sidecars are held owner-only, and no answer, no prompt text, and no label ever travels on the dashboard's change stream - only ids and a status.

A record the store cannot read is shown, not skipped. It appears in orgabot inputs and on the dashboard marked unreadable rather than dropping out of the list, because a question that silently disappears is a mission waiting for an answer you were never asked for.

Implement admission queue

Before an org-routed implement worker starts, Orgabot checks the five-minute host load and free-memory headroom. A constrained launch enters the existing FIFO throttle queue, so it creates neither a worker nor a worktree. The dashboard shows its position and current reason and automatically retries the oldest launch as capacity returns. Full and light verification remain independently bounded by the host-global verify slot pool.

Defaults target a laptop: at least 1 GiB and 5% memory free, with five-minute load no higher than twice the logical CPU count. Override them for the host running Orgabot:

export ORGABOT_IMPLEMENT_MIN_FREE_MIB=2048
export ORGABOT_IMPLEMENT_MIN_FREE_PERCENT=10
export ORGABOT_IMPLEMENT_MAX_LOAD_PER_CPU=1.5

What is limiting throughput

Insights -> Profiler leads with one sentence: the binding constraint, and what to change about it.

That is a different thing from the numbers underneath it. Peak load was 171 is a measurement; your missions spend 40 minutes each in the forge's check queue, so nothing on this host will help is an answer. The classes it distinguishes:

ConstraintWhat it means
MEMORY_BOTTLENECKThe host is compressing and swapping. Every other reading is a symptom.
VERIFY_BOTTLENECKVerifications are queueing for a host slot.
CI_BOTTLENECKLocal is healthy; the forge is the queue.
CONTROL_PLANE_BOTTLENECKOrgabot's own processes are the floor missions compete under.
NO_BINDING_CONSTRAINTEverything was measured and none of it binds. Push harder and look again.
INSUFFICIENT_DATANot enough was measured to say. It names what was missing.

Those last two are deliberately different answers. One says take more measurements; the other says the measurements are there and nothing is holding you back.

ADR 0061 also sketches a MODEL_WAIT and a DEPENDENCY_PREP_BOTTLENECK. Both are computed from per-mission local compute, which nothing records yet, so neither is listed here: a constraint that can never fire is an answer the page advertises and can never give, while the operator whose fleet has that problem is told nothing is binding. They arrive with the measurement they need.

The order is the diagnosis. Memory comes first, because a swapping host makes every other reading meaningless. Then the two MEASURED waits, verification and CI, and last the control plane, which is the floor under all of it. Nothing here is computed as a remainder: a remainder counts queue and forge time as something else, so a mission that spent an hour in the check queue would be diagnosed as network-bound and answered with more concurrency, which deepens the very queue it is stuck in.

The verify figure is the worst project's median wait, not a fleet average. An average across a quiet project and a contended one describes neither, and would send you looking for a fleet-wide problem that is one repository's.

Three rules keep it from being confident noise:

  • An absent signal is not a zero. A host with no memory reading is not a host with no memory pressure. A constraint whose evidence was not measured is not ranked, and "we could not tell" is said plainly rather than dressed up as the most plausible-sounding constraint that happens to have numbers. That applies to the all-clear too: if the queue waits went unmeasured in a window, you are told they did, rather than told that nothing binds. A fleet that verifies locally has not waited zero minutes on CI, it has not waited on CI at all, and the two must not average together.
  • Every conclusion carries the figures it stands on, so you can disagree with the reasoning rather than only with the verdict.
  • It recommends changing the constraint, not the ask. "Run 2-3 missions" is the advice this replaces. Where a real constraint is identified, the recommendation names it: add a light verify slot, free memory, move a phase to CI authority, or restart the dashboard.

Scheduler presets

Laptop, Workstation and CI-offloaded are useful names, and as fixed numbers they are folklore: one full verify slot is right for a 16 GB laptop and timid on a 128 GB workstation, and neither figure knows which machine it is on.

orgabot verify preset                      # in force, what this host suggests, live limits
orgabot verify preset --set workstation    # apply one
orgabot verify preset --suggest            # a recommendation, from what has been observed

This sets local slot counts only. No project's verification authority moves. The similarly named orgabot project verify-policy <project> --preset ci_primary is the one that moves pre-ship verification to the forge; the scheduler's ci_offloaded preset only sizes local slots on the assumption that it already has.

orgabot verify preset with no flags names the preset in force (the last one you applied with --set) as well as the one this host suggests. They are different questions and both are answered.

The preset is derived from detected CPU count and physical RAM, coarsely and on purpose: this is a starting point an operator adjusts, and a precise-looking number derived from two figures would imply a confidence the derivation does not have.

Three things it will not do:

  • Infer CI-offloaded from a machine's size. That is a statement about where a project's authoritative verification runs, which is a decision about a repository and its forge. Nothing about a host implies it, and guessing would silently move a gate.
  • Treat an undetected host as average. Absent is unknown, and the safe reading of unknown is the smallest machine anyone runs this on, so an undetectable host gets the conservative floor.
  • Give a laptop a second full verify slot, however much RAM it reports. Two full suites at once is the thrash this whole area exists to remove, and more memory does not change that while the machine is also yours. The detection thresholds sit where portable silicon stops rather than where "big" starts, so a 16-core, 128 GB laptop is still a laptop.

Suggestions are never applied

--suggest reads what has actually been observed and prints at most one change, with its reasoning. It does not apply it. A scheduler that adjusts its own caps every sample interval is one an operator cannot reason about, and a list of simultaneous adjustments makes it impossible to tell which change caused what.

Both directions ask the same question - would that many concurrent full suites fit - from the suite's own observed peak memory. What differs is what they measure it against.

Lowering measures against the host's total size. Telling you that your configured slots would not fit even an empty machine is safe advice whatever else is running.

Raising asks both questions and needs both answers. What is actually free right now bounds the immediate risk; the host's total bounds the absolute number. Measuring only against total models your machine as idle - a 64 GB Mac already holding 40 GB would be told a second suite leaves "88% of the host free". Measuring only against what is free ratchets: run --suggest on a momentarily quiet machine and it would propose one more slot every time.

It also declines entirely when nothing measured what is free, and "nothing" is stricter than it sounds. freemem() is not that measurement: on macOS it counts only genuinely free pages, so a healthy 64 GB Mac reports about 1.5 GB while the kernel reports 54 GB available, and on Linux it is MemFree rather than MemAvailable. A raise is offered only when the platform gave a real pressure reading. Adding load to a host nobody looked at is the unsafe direction.

It never decides from a host memory percentage. used = total - free is not a pressure reading: macOS counts reclaimable pages as used, so a perfectly healthy Mac sits above 90% at rest. A calibration reading that figure would advise every workstation back down to one slot on day one, with a swapping rationale the number does not support, and could never suggest a raise at all. Anything it did not measure, it declines to suggest from.

--set and --suggest cannot be combined: applying a preset first would mean suggesting against limits nothing has run under yet.

What verification costs, and what the queue tells you

Orgabot records what every local verification actually cost, keyed by a fingerprint: the project, the exact command list, the profile (light or full), and the runtime and platform it ran on. Two projects with the same command are different fingerprints, and so are the same project's light and full suites.

From that history it keeps median and p95 wall time, p95 peak memory, median queue wait, and how often the suite failed. Inspect it with:

orgabot verify costs                    # every recorded fingerprint
orgabot verify costs --project my-repo  # one project
orgabot verify slots                    # the live host slot pool
orgabot verify costs --clear            # start the history over

The same picture appears under Insights -> Profiler, as "What verification costs here".

This is what lets a queued mission say queued for full verify (position 2; held by mission-abc since 2026-08-26T15:04:11Z; this full suite usually takes ~7m) instead of only a position.

Note what it does not say: when the run will start. The runs ahead belong to other missions with other suites, and the queue records do not carry their fingerprints, so any start time would be a guess dressed as arithmetic. What this suite costs is a fact, it is what you are actually deciding on, and it does not go stale. Two more rules keep the figures honest:

  • An estimate is never evidence. Nothing here decides pass or fail, and no gate is skipped, shortened, or waived because a suite looks expensive. Cost history feeds queue ETAs and admission packing; the verification authority, profile and phase model is untouched by it.
  • Unmeasured reads as unmeasured. A fingerprint with no history reports no numbers rather than a zero, and a run whose process table could not be read contributes no memory figure rather than a 0 MB one. While a history is thin, the estimate quotes the p95 rather than the median on purpose: an overestimate makes a mission wait, an underestimate admits work onto a host that cannot hold it.

Verifying only what changed

For a monorepo whose full suite costs minutes and gigabytes, Orgabot can ask the project's own build system which targets a change affects, and verify those instead of everything.

It asks the build system. It does not keep a private model of your dependency graph, because a wrong model is not a smaller gate: it is a gate that runs the wrong suite and reports the result as though it had run the right one.

Supported today: turbo, nx, pnpm and bazel, which are the tools with a first-party affected-targets query. Cargo and Gradle have none, so they are declared unsupported rather than approximated.

Opt in per project, under runtime.verifyPolicy.targeted:

{
  "tool": "turbo",
  // {targets} is replaced with the tool's own filter syntax for the selection.
  // A template without it is refused: it would run the whole repo and record
  // the result as a targeted run.
  "command": "turbo run build test {targets}",
  // Which phases may use a targeted run. pre_ship is never included by
  // default: it is the authoritative gate.
  "phases": ["post_repair"],
  // Does the targeted set count as the FULL profile for those phases?
  // Literal true only. Absent means a targeted run records the light profile.
  "authoritative": false
}

Absent means no targeting, which is how every project behaved before this.

Everything uncertain runs the full suite

A targeted run that is too wide costs compute. One that is too narrow is a pass for code nobody measured. So every failure mode resolves the same way, and says so in the mission log:

  • no configuration, or an unsupported build tool;
  • a phase the project did not nominate;
  • no base ref to compare against;
  • the graph query failed, could not be started, or timed out;
  • its answer could not be read;
  • it named a target outside the safe shape - which refuses the whole selection, never just that entry, because a partly honoured selection is a targeted run over a scope nobody chose.

What the evidence says

A targeted run records the light profile unless the project marked the targeted set authoritative for its phases, and its summary names the tool, the base ref, and the selected targets. A full pass over three packages and a full pass over the whole repository are different claims about the same commit, and the evidence has to be able to tell you which one you have.

An empty selection - the build system positively answering "nothing affected" - runs no commands and is recorded as a skip that names that answer. It is not a pass: a gate passes on command exit-code evidence, and no command ran.

Verifying several missions at once (dry run only)

Three missions changing three different packages off the same commit could, in principle, be verified together:

base + A + B + C  ->  run the expensive suite once

On a monorepo whose suite costs seven minutes, that is three runs collapsed into one. The trap is what happens when the combined run fails:

A alone?  B alone?  an A+B interaction?  C?  or was the baseline already red?

Nothing in the combined result answers that. So coalescing is not on, and this release ships only the thing that lets you decide whether it is worth turning on for your workload:

orgabot verify coalesce               # what would batch right now, and why the rest would not
orgabot verify coalesce --project app

It runs nothing, applies nothing, and writes nothing. It reports which live missions would form a batch (disjoint changes, one shared base, one identical suite), and names every mission that would be left out with the reason: an overlapping file, a different base, a different command list, a change set it could not read, or uncommitted work. That includes the case where no batch forms at all - "no batch would form" with the reasons thrown away is not an answer to the question you asked.

It also tries every candidate as the starting point and keeps the largest batch that actually results, rather than the largest group that merely shares a base and a command list. Three missions that all touch one file share a base and a command list and can still batch nothing, and anchoring on them would discard a genuinely disjoint pair sitting right beside them.

Two of those are about what a change set cannot see. A change set it could not read is refused rather than treated as empty, because a mission that appears to have changed nothing appears disjoint from everyone. And a mission with uncommitted work is refused for the same reason: the comparison is over committed history, so paths its worker has touched but not committed are invisible to it. Renames are read on both sides, since git otherwise reports only the destination and a mission that deleted a file would look disjoint from the mission still editing it.

A coalesced result would never be a mission's authoritative verification. The asymmetry is the whole design: a coalesced PASS is a real fact about every member, since the suite passed with all their changes applied. A coalesced FAIL is a fact about the set and attributes to nobody, and attributing it costs the baseline plus one run per member, which is more than the batch saved. The per-mission gate stays exactly where it is.

Where work could run

ADR 0061 keeps remote workers out of scope for the local throughput problem while asking that the architecture stop preventing them. So Orgabot declares the placement targets a schedulable task could have, and orgabot doctor prints them:

TargetWhat it is
localThis machine, under the verify slot pool and the resource governor.
ciThe forge, for a full verification a project has said CI may answer.
remoteDeclared, and never selected. Nothing implements it.

This is a vocabulary and a decision function, not a dispatcher. No task is routed through it today, and there is nothing new to observe on the dashboard. What exists is a single place where the question "where should this run?" is answered explicitly, so the next thing that needs to ask it does not invent its own rules.

These are not the same thing as the egress planes orgabot connectivity reports (local, claude-cloud, modal). Those are about where a worker's network traffic goes. "No remote placement target" is not a claim that those do not exist.

The one placement decision Orgabot actually makes today lives in the verify scheduler: a full local verification under sustained host pressure may defer to CI, when the project has said CI is an acceptable authority for its evidence (verify.pressurePolicy.allowDeferredCI). Sustained is load-bearing - a single refused reading is not evidence the host cannot cope, so the scheduler buys a bounded grace wait before it concludes anything, and only pressure that outlasts that window defers.

Two things the model refuses, and they are the parts worth keeping:

  • Only a full verification has more than one target. An implement worker on CI is a different product, and a dependency install there produces nothing this host can use, so both stay local whatever the pressure.
  • A queue length is never a reason to change what a gate stands on. CI is reachable only for a project that has said CI may answer for its evidence, and an unanswered question about authority is not a yes. A CI placement is also marked as not producing pass evidence: awaiting_ci is not a pass until the required checks are green for that exact commit.

Capacity pauses

Sustained host pressure (critical or emergency, held over a real recovery window rather than one noisy reading) asks a running mission to pause cooperatively. The request never interrupts a worker or a verify command in flight: it lands as resourceControl.state: "pause_requested" on the mission record, and the mission's own process honors it (resource_paused) the next time it reaches a safe boundary - preserving the branch, worktree, session, and every commit already made.

That boundary is checked far more often than just between the implement, verify, review, and deliver stages. Inside the verify-repair loop and the review-fix loop, it is checked before every repair or fix round starts and before every full-suite verification run (including each re-verify after a repair), not only once the whole loop finishes. A pause requested mid-loop is honored at the very next round boundary rather than waiting out however many repair-and-reverify cycles remain.

Two background passes on the dashboard's own timer keep a pause request from ever going stale:

  • Withdrawal on recovery. A request the mission process never reached before the host recovered is cleared automatically once a full recovery window of calm readings is established, so a mission does not pause on an already-healthy machine hours later.
  • Settlement with no worker. A request stamped on a mission with no live process to honor it - no tracked pid, a dead pid, or a controller parked waiting on a dependency or a hold - is settled every tick instead of sitting in pause_requested forever: it is recorded as honored at once (resource_paused, "paused without a worker; nothing to stop") while the mission is still in flight, or cleared once the mission reaches a terminal outcome. The dashboard never shows a pause_requested older than one tick of this pass on a mission with no owner left to act on it.

The runtime memory watchdog

Admission control decides whether work may START. The runtime memory watchdog watches the host WHILE missions run, because memory pressure that arrives after admission is exactly what a launch check cannot see: another application (a local model, a VM, a browser) can take tens of gigabytes after Orgabot's missions were admitted, and Orgabot's own builds keep growing.

It runs inside the orgabot up supervisor and samples every 5 seconds. It never runs from the dashboard, so killing the dashboard neither stops it nor changes what it does. Every reading is either measured or absent; an absent reading is never treated as zero, and a sample with no memory signal at all neither pauses anything nor counts as recovery (an emergency from an independent signal, below, still fires).

What it reads

SignalSourceElevatedCriticalCounts as recovered at
Available memorykern.memorystatus_level (or memory_pressure -Q)under 20%under 10%30% or more
Kernel VM pressure levelkern.memorystatus_vm_pressure_level2 (warn)4 (critical)1 (normal)
Compressor occupancyvm_stat35% of RAM50% of RAMunder 30%
Compressor growthbetween two samples128 MiB/s512 MiB/s32 MiB/s or less
Swap in usevm.swapusage (current, not cumulative)25% of RAM75% of RAMnot growing (within 512 MiB of its low in the last 2 minutes); the level itself never holds a resume
Free space on the swap volumethe volume behind /private/var/vmunder 25 GiBunder 15 GiB (emergency)30 GiB or more
A new JetsamEvent-*.ips report killing for system pressure/Library/Logs/DiagnosticReportsalways (emergency)2 minutes after it

The worst signal decides the level. Pressure is judged on the whole host, whoever caused it: your own interactive work always wins, and missions are the load that can wait. A host squeezed entirely by a process Orgabot did not start still pauses Orgabot's work.

A jetsam report counts only when it killed processes for system memory pressure (low-swap, vm-pageshortage, vm-thrashing, fc-thrashing). A report whose only kill is one process over its own limit (per-process-limit) or an idle exit says nothing about the host; it is recorded in the forensic log and decides nothing.

What it does

  • Elevated: no NEW work starts. New launches, verify slots, dependency preparation, background sweeps, and every resumption (throttle-queue drain, interrupted-mission recovery, held-mission resumes, open-PR resumption) wait and are re-offered as soon as the host recovers. Nothing already running is touched. The wait is never silent: a queued launch records why, and a verification waiting for a slot prints "memory watchdog reads host memory elevated" on the mission.
  • Critical: only after two consecutive critical samples (three when compressor growth is the only critical signal, since one noisy delta is not evidence), running missions are paused, the lowest priority first (background work before missions you started from the dashboard or CLI, and among equals the one holding the most memory), one mission every 10 seconds so the next reading decides whether more must go. Pausing a mission first kills and requeues its re-runnable subprocesses (verification, build and test runners, security scanners): their results are re-derivable evidence, and killing them is what actually returns memory. The same command re-runs from scratch after the pause, so a gate only ever sees a complete run's exit code; a killed run is never recorded as a pass or a failure. The worker session parks at its next safe boundary. If critical pressure is still there 30 seconds after that mission's own pause, its coding-worker process groups are suspended (SIGSTOP) until release, with the worker's idle limit held so it is never killed for being silent. Only the worker sessions' process groups are ever stopped: never the mission's own process, and never a git, gh or push the mission itself is running.
  • Emergency (a system-pressure jetsam report, or the swap volume under 15 GiB): every running mission is paused at once and its worker groups suspended, without waiting for a second sample.
  • Recovery: a pause is released only after a sustained window (2 minutes of contiguous samples, all past the "recovered" column above, which is deliberately stricter than the pause thresholds), and only one mission at a time, a minute apart, the most protected first. New work waits until every paused mission is back.
  • Swap in use is not a reason to stay paused. macOS keeps swapped pages swapped until something touches them, so after a short spike the amount of swap in use can stay high for hours while every other reading is calm. Its level therefore never holds a resume: only swap that is still growing (more than 512 MiB above its lowest reading in the last 2 minutes) does. Swap at 25% of RAM or more still holds new work, and swap at 75% of RAM or more still pauses missions.
  • A held resume says why. While a mission is paused and not yet released, orgabot throttle watchdog, orgabot throttle status, and the dashboard's memory watchdog section list what is holding it (for example "swap growing", "compressor at 32% of RAM", or how far into the 2-minute recovery window the host is), and since when.

What a pause is, and is not

A pause is recorded in the mission journal as a ResourcePaused event carrying the reading that triggered it (available memory, VM pressure level, compressor size and growth, swap, swap-volume space, the mission's own resident memory, and the jetsam report if there was one), and released by ResourceResumed. The watchdog re-checks the journal against the mission record every sample, so a journal write lost to a busy store is repaired rather than left disagreeing. It is not a hold: nobody is asked anything. It is not a failure, never a review-loop strike, and it does not consume the review round budget. The mission keeps its status, its open round, its worktree, and its branch, and still owes every verify, review, and merge gate it owed before. Recovery and open-PR sweeps leave a paused mission alone: it is never reported as hung, merged, failed, held, or relaunched while paused. The dashboard shows it with the pause glyph and the triggering reading in the tab's hover text, beside the memory its process tree holds.

orgabot mission resume <mission-id> (or the dashboard's resume action) ends a pause early; the release is journaled as the operator's, and the watchdog will not re-pause that mission for 10 minutes unless there is an emergency.

A pause never outlives the watchdog

Only a live watchdog can end a watchdog pause, so a pause is never left without one:

  • Stopping the supervisor in an orderly way releases every pause it holds and withdraws its verdict.
  • If the supervisor dies or stops sampling, a paused mission releases itself once the watchdog's last reading is 3 minutes old (or at once if the supervisor process is gone), journaled as "memory watchdog unavailable". The same bound reopens admission: a stalled sample during pressure keeps new work held for up to 3 minutes rather than waving it through, and never longer.
  • A mission ended while paused leaves its pause at once, and the watchdog clears the record.
  • If a mission's own process dies (killed, or jetsammed) while its worker groups are suspended, the supervisor finds the stopped set that process recorded under ~/.orgabot/runtime/watchdog-stopped/, confirms the groups are still the same processes (by start time, never by pid alone), and resumes them so the ordinary orphan cleanup (orgabot clean) can end them.

Turning it down, or off

VariableEffect
ORGABOT_MEMORY_WATCHDOG=observeSample and record what it WOULD do, without holding or pausing anything
ORGABOT_MEMORY_WATCHDOG=offDo not run it at all
ORGABOT_MEMORY_WATCHDOG_ELEVATED_AVAILABLE_PERCENTAvailable-memory elevated threshold (default 20)
ORGABOT_MEMORY_WATCHDOG_CRITICAL_AVAILABLE_PERCENTAvailable-memory critical threshold (default 10)
ORGABOT_MEMORY_WATCHDOG_RESUME_AVAILABLE_PERCENTAvailable memory that counts as recovered (default 30)
ORGABOT_MEMORY_WATCHDOG_SWAP_VOLUME_ELEVATED_GIBSwap-volume free space that holds new work (default 25)
ORGABOT_MEMORY_WATCHDOG_SWAP_VOLUME_EMERGENCY_GIBSwap-volume free space that pauses everything (default 15)
ORGABOT_WORKTREE_MIN_FREE_GIBData-volume free space below which no new worktree is created (default 25)

Anything unrecognized in the mode is treated as the default (enforce), so a typo never switches protection off. Out-of-range thresholds keep their defaults, and an override can never make a pause threshold looser than its recovery threshold. These are operator settings: a worker can never be granted any of them.

Forensics that survive a reboot

Every sample (host signals, per-mission resident memory, how much memory belongs to nothing Orgabot runs, every jetsam report seen, and every decision taken) is appended to an hourly file under ~/.orgabot/diagnostics/memory-watchdog/, kept for 48 hours and capped per hour. Read the last hour back with:

orgabot throttle watchdog               # verdict, per-minute readings, per-mission peaks, decisions
orgabot throttle watchdog --minutes 180 # a longer window
orgabot throttle watchdog --json        # the same report as JSON (also GET /api/watchdog)

orgabot throttle status prints the current verdict in one line, and the dashboard's Profiler page shows the same last hour.

Disk is capacity too

macOS keeps its swap files on the Data volume, so a disk filled by worktrees turns memory pressure into low-swap kills. The disk numbers line up on purpose:

  • Under 25 GiB free: no new worktree is created (a launch queues, with a reason pointing at orgabot clean --attribute and orgabot clean), and, since the swap files share that volume, the watchdog reads it as elevated and holds all new work.
  • Under 15 GiB free: an emergency; every running mission pauses.
  • 30 GiB or more counts as recovered.
  • Well before any of that, when Orgabot's own worktrees are at least as large as the free space left and less than 50 GiB is free, the watchdog's verdict says so and names the same commands.

Reusing a verification instead of repeating it

When a local verification has already passed against exactly the same inputs, Orgabot can reuse that result rather than running the suite again.

Exactly the same means all of it: the same repository, the same commit SHA, the same profile, the same command list, the same lockfile hash, the same runtime version, and the same platform. Change any one and it is a different key and a different answer.

orgabot verify cache            # what may be reused, and the commit each is bound to
orgabot verify cache --clear    # forget all of it

It is built to miss

A false miss costs compute. A false hit reports a pass for something nobody measured. So every one of these is a miss, and none is an error:

  • any part of the key that could not be read: an unresolvable commit, an unknown runtime, an absent lockfile hash. "We could not tell whether the dependencies changed" never resolves to "they did not";
  • a ref instead of a commit. Branches and tags move, and a key a moved ref can satisfy answers for code nobody ran;
  • a stored entry that does not hash to the key it is filed under. The row carries the command digest, lockfile hash, runtime and platform it was built from, so the key is re-derived and compared rather than taken on the row's word;
  • an unconfirmed HEAD. If Orgabot could not read the worktree's commit, the run is not filed at all: the fallback is the base commit, which is a real SHA that simply is not this tree, and a pass filed under it would be served to the next mission off the same base;
  • anything older than the retention window, or dated ahead of this clock;
  • and any difference at all in profile, commands, lockfile, runtime or platform.

It cannot cache a failure, and it never trusts narrative

Only a local run observed to pass is ever stored, and the recording function has no parameter for an outcome, so there is structurally no way to store a red run and no path from "the agent said the tests passed" into the store.

A reuse is recorded as evidence with provenance, never as a bare pass: the mission's summary names the run it stood on and when that run happened, so a reader can always see that this gate did not execute here. It also carries the command the original run observed, so a reuse is never mistaken for the separate case where no verification was configured and nothing ran at all.

orgabot verify cache counts the entries that would actually be reused and names the rest individually, and --clear distinguishes a store that is busy from one it could not write: "try again in a moment" fixes contention and will never fix a permissions problem.

One project cannot hold the host

Verify slots are host-global, which is what stops five missions running five full suites at once. It also creates a way for one repository to starve every other: with a single line and arrival order, a project with twelve missions steadily producing verify work sits ahead of the one mission on a small project, and that operator watches a queue position that never improves.

So the queue is ordered by turn, not by arrival. The default policy is weighted fair queueing by project: the projects with work rotate, and within a project the oldest request goes first.

Concretely, with alpha holding a slot and a1 a2 a3 queued behind it plus one b1 from beta, the order is a1, b1, a2, a3. Alpha's oldest request still goes first, because it genuinely has waited longest; what changes is that beta takes the very next turn instead of waiting behind all of alpha.

Three properties keep this from becoming something else:

  • It reorders; it never adds capacity. The ordering is a permutation of the requests already waiting. It cannot admit work the host refused, cannot raise a slot count, and there is no way for a project to skip the line.
  • The wait is bounded, not merely fair on average. A project with a request waiting more than 15 minutes gets one promotion ahead of the rotation, oldest first, and then yields its turn. Rotation alone can be persistently unkind to one request; age is what makes it a bound. One per project, and yielding after, because a twelve-deep backlog on one repository ages past the threshold as a block: promoting all of it, or promoting the same project again next time, is arrival order again with extra steps.
  • The position you are shown is the position in promotion order, and it comes with the reason: verify slots rotate among the 3 projects with work, or promoted ahead of the rotation after waiting over 15 minutes. It is re-announced whenever it changes, so the number in front of you is current rather than a snapshot taken when you joined the queue; under a rotating policy the order moves while you wait. A number that does not predict when your run starts is not worth showing, and scheduling that feels arbitrary is indistinguishable from a hang.

orgabot verify slots prints the same order the pool will actually take, and orgabot doctor counts a project as having work whether it is queued or holding a slot, which is the count the rotation itself uses.

Set hostVerify.fairness in ~/.orgabot/config.json to fifo for the older arrival-order behaviour. An unrecognized value reads as the default rather than as fifo: a typo in one config field should not silently reinstate the starvation this exists to remove.

Dependency preparation is scheduled too

A mission's isolation is paid for with its own node_modules, cloned before the worker starts. Starting six missions at once used to start six recursive copies on one disk at the same instant. Provisioning now takes one of a small number of host-global dependency preparation slots (two by default), admitted by the same host resource governor that admits implement and verify work, so orgabot throttle status shows DEPENDENCY_PREP beside the other classes.

A busy pool delays provisioning; it never skips it. If the wait is exhausted, the copy runs anyway and says so in the mission log, because a mission with no isolated dependencies is the one whose worker reaches for the host's.

The tree itself is cloned from a shared, content-addressed dependency store under ~/.orgabot/depstore, keyed by the project's lockfile and materialized once per host. Every mission still gets its own independent copy (copy-on-write where the filesystem offers it), never a link into a shared tree, so no mission can ever mutate another's dependencies or your own checkout. Set ORGABOT_SHARED_DEP_STORE=0 to take the store out of the path and clone directly from the project checkout instead.

An entry has to earn its place in that store, because it is shared across every project on the host: the tree is checked against its lockfile before it is filed and again, in full, before it is served. A tree missing a package the lockfile lists, holding a symlink that points outside itself, or whose files have changed since it was filed is refused. Because another project's mission is served the same entry and will not re-install it, a tree installed with --omit=dev is refused too: complete for the checkout that made it, incomplete for the next one. The store directory itself is created owner-only (0700).

A refusal is not an error: the mission simply clones directly from the project checkout, exactly as it did before the store existed. The check needs a lockfile Orgabot can enumerate, so today package-lock.json projects use the store and yarn/pnpm projects always clone directly.

The mission bar counts queued launches too. A queued mission has no session yet, so it gets no tab there; instead an amber `N queued` counter sits beside the quota meter, scoped to the project you have selected and hidden entirely when nothing is queued.

Click the counter to expand the queue. Each waiting launch is listed oldest first (the order they will actually start in) with its instruction, its project, how long it has waited, and why it is waiting: token bandwidth, a free mission slot, or host resources. Every row has its own Cancel. Press Escape or click away to collapse the list again.

Cancelling from that list is the same cheap stop as the CLI: nothing has started, so there is no worker, worktree, or branch to lose, and the mission the launch would have created is marked cancelled at the same time. A launch the queue has already begun releasing cannot be cancelled this way: the list will say so and put the row back.

From the terminal, inspect, retry, or cancel a queued launch with orgabot throttle queue, orgabot throttle resume-due, and orgabot throttle cancel <entry-id>.

orgabot throttle resume-due (and the automatic drain) looks at every queued launch, not just the oldest one, and starts each one whose limit has cleared. What a refusal holds back depends on the limit that refused it right now:

  • A launch waiting on another mission's project write lease, or on a dependency, holds back only itself. Unrelated launches behind it still start.
  • The organization's token bandwidth, mission slots, or budget holds back that organization's other launches for the rest of the drain (they would be refused the same way). Other organizations' launches are still tried.
  • Host load or memory holds back every launch, and the launches it skipped get the current reading recorded, so orgabot throttle queue never shows a host-pressure reason from hours ago. throttle queue also shows host pressure as it is now.

If a launch is waiting on a mission whose own next round is also sitting in the queue, the two would wait on each other forever. The drain starts the lease holder's round first (it already owns the lease) and prints a lease deadlock line naming both queue entries. If the holder's round still cannot start, the line says why.

The output lists every launch still queued with its current reason: the ones tried and refused, and the ones marked not attempted because an earlier refusal in the same drain already covers them.

Queued is its own status

A mission admission control has not started yet reports as queued everywhere: orgabot status, the dashboard Missions list and its status filter, the issue card of the issue it was launched for, and the mission's own record and journal. It is not running (there is no worker), not waiting (nothing is waiting on you), and above all not cancelled (nothing was discarded). Its summary names which limit was hit, the queue entry that owns its relaunch, and the two commands that inspect and release it. One of the limits it can name is the host-wide admission budget: every kind of concurrent work (verification, security scans, coding-worker sessions, review/ship loops) shares one fixed, host-wide cap, checked before the work starts, so a launch is queued rather than started and later paused by the capacity governor.

A queued mission occupies no concurrency slot, so a burst of queued launches never wedges the cap. When capacity returns, the same mission id relaunches, and its status moves straight to running. If you cancel the queued launch yourself with orgabot throttle cancel <entry-id>, the mission it would have started is marked cancelled at the same time, so nothing is left parked in Queued waiting for a relaunch that can no longer happen.

Each queue entry belongs to exactly one mission. Two launches with the same instruction, role and project (for example, two restarts of the same failed mission) get two entries, and releasing an entry starts the mission that was queued on it, never a different one. A queued mission is not treated as an interrupted one either: it has no branch and no worker by design, so recovery never tries to "restart" it and leave it where it was. Instead the recovery sweep checks the queue entry the mission is waiting on:

  • If the entry is still waiting, or has just started this mission, the mission is left alone.
  • If the entry was released as a different mission (an older install could rebind an entry this way), the queued mission is marked cancelled as superseded, and its record names the mission that ran in its place.
  • If the entry was cancelled, the mission is marked cancelled too.
  • If no entry is left that could ever start it, the mission is held with that reason (queued_launch_orphaned), so it shows up as needing your attention instead of sitting in Queued forever.

How a mission turned out is a separate field from where it is

running, waiting, queued, completed, failed and cancelled answer where a mission is. Beside that, a mission that has ended also records how it turned out, as its own field with four values. The two are deliberately separate, because one field answering both questions makes a mission that did everything it safely could and stopped at a gate you own look exactly like one that crashed.

OutcomeWhat it means
completedThe objective was achieved autonomously.
needs a humanA success. Every safely executable piece of work is done, and an essential action only you can take remains: an approval the policy reserves, a credential only you hold, a capability grant the role does not have, a decision nobody delegated.
blockedAn external condition no immediate action of yours resolves: an upstream work item that is not done, a third-party outage, an executor pool whose quota window has not rolled over yet.
failedAn actual orchestration or execution failure. Reserved for that, so it stops being the bucket for "something other than completed happened".

orgabot status prints the outcome on its own line under the mission, with the human-owned actions listed beneath it when there are any. The dashboard shows it as a second pill beside the status pill (needs a human and blocked read as the amber Needs a human and Blocked attention pills, since both wait on something outside the mission), and the card's colour follows the outcome rather than the status, so a mission whose status settled on failed while its outcome is needs a human no longer paints red.

Two cases record no outcome at all, and the absence is the point:

  • A mission you cancelled. Cancellation is a decision of yours, not one of the four; the status already says cancelled.
  • A mission that died on a network, DNS, or timeout failure. That says nothing about whether the work succeeded, so it is a retry rather than a verdict, and recording failed for it would be a judgement nobody observed.

Nothing about completed changed meaning. It still requires the same evidence it always did, and a completed that contradicts this mission's own observed verification failure is still downgraded exactly as before. The outcome is derived from the status that survives that check, so the two can never disagree with each other.

Every mission ends with a summary that says why

The summary on the mission record is what the MISSION OUTCOME: line, the dashboard card, and the mission terminal's Outcome block all read. It always states what happened and why, including when the mission changed nothing.

A code mission that ends with an empty diff has more than one cause, and they need different responses from you:

  • Already satisfied. The work is already present at the base commit. Orgabot reports this only after confirming it itself: the worker's report says which files to look at, and Orgabot goes and looks. A claimed path:line counts only when the file exists at the base commit and is at least that long. A worker saying "already implemented" is never enough on its own.
  • No changes produced. The run genuinely changed nothing. The summary carries the worker's own account of why, truncated but never discarded. If the worker claimed the work was already in place and Orgabot could not confirm it, the summary says exactly that and does not read as "already fixed".
  • Blocked, or a round answering a rejected fix, holds as waiting rather than completing. Those are unchanged.

For a mission launched from a GitHub issue, the zero-diff conclusion is also written back to that issue. An unexplained empty run (the "no changes produced" case above) still just comments: the issue is still unaddressed. An already-satisfied round goes one step further and asks a second question: does the worker's report name a commit or pull request, and does Orgabot independently confirm it? Confirming means five separate facts, all read first-hand from git and GitHub rather than from the worker's report: the cited commit (or the cited pull request's merge commit) is an ancestor of the base commit this round inspected; that commit, or the pull request's own title and body, declares that it closes this issue with a closing keyword bound to this issue's number (Closes #1906, Fixes owner/repo#1906); the work it left is still byte-identical at the base commit in at least one of the files Orgabot confirmed there; and the issue's own current text states no outstanding requirement: no unticked acceptance checkbox, and none of the epic / design / p0 shapes Orgabot never auto-closes on a single delivery; and the same child-issue check a delivering pull request goes through admits a Closes for this issue, so an issue whose GitHub sub-issues (or whose children listed in its body) are still open is left open here exactly as it would be on a merge. Any one fact missing means no close. Ancestry alone would prove nothing at all: every commit in a repository's history is an ancestor of the base, including its very first one, and a fix stays an ancestor after it has been reverted. A bare mention is not enough either, which is why the closing keyword is required: a commit titled Add regression test for #1906; authorization fix remains outstanding names the issue, is not a revert, and leaves its test file untouched at the base, so mention-level linkage would have closed an issue whose actual ask was never implemented. And because the acceptance check reads the issue itself, an issue whose text cannot be read leaves the issue open rather than closing it on an unknown. A commit that withdraws the work it names is refused outright, however completely it satisfies those three facts: a revert's subject quotes the subject of the commit it removed, so it names the issue exactly as the fix did, and if it is the tip of the base commit it is its own ancestor with its own files unchanged at the base. Its content is the absence of the fix, so Orgabot comments and leaves the issue open. "GitHub says merged" is deliberately not enough either: a pull request that landed after this mission took its base snapshot, or whose merge commit is no longer in that history, is absent from the code the worker actually read, so it confirms nothing and the issue stays open. Only when that confirmation succeeds does Orgabot close the issue itself, with a comment naming the confirmed commit or PR, through the mission's governed GitHub identity (the connector app assigned to the mission's org/role, or gh for a mission with none). A role-owned mission whose connector is unavailable holds rather than falling back to your personal gh identity, exactly like every other governed write-back. When the worker's claim carries no citation, or Orgabot cannot confirm the one it does carry, Orgabot comments "reported as already satisfied by the worker, not confirmed" and leaves the issue open, exactly as before: closing on unconfirmed evidence is not something the runner does. Either write-back is posted once per mission, recognised on a retry rather than repeated: the marker that recognises it is signed with a key on your own machine and tied to that one mission, repository and issue, so another app commenting on a public issue cannot make Orgabot skip a close. And a close is only ever reported as one after Orgabot re-reads the issue and sees it closed. If that read fails, or the issue is still open, the round holds and says so rather than recording the mission complete over an open issue.

A confirmed, closed already-satisfied round reaches a terminal completed mission with a "no change needed" outcome, never waiting, since there is nothing left for you to act on. An unconfirmed round is not parked on the spot. When Orgabot checked the claim and refused it (the issue was told "not confirmed"), the mission runs one follow-up round in the same worktree, with the rejection in the worker's brief: what was claimed, which paths were cited, and why that did not stand, along with the reminder that a document describing the defect (an audit finding, a plan, the issue text) is never evidence the defect is fixed. The worker either implements the change, so the round goes on through the ordinary commit, verify, review and delivery path, or cites evidence Orgabot can confirm. A second unconfirmed claim holds the mission as waiting on already_satisfied_unconfirmed, with a summary that says the claim was not confirmed and why (finding a cited file at the base commit shows the file exists, not that the work is done), and it holds without the project's write lease: a round that left its branch at the base commit has nothing unmerged to serialize, so sibling missions of the same epic (or on the same paths) are no longer queued behind it until a merge or a terminal that never comes. A claim that could not be checked at all (the mission's connector was unavailable) still holds, and that hold is lease-free too. A resume of any mission whose lease was released this way, or at a terminal (a retry of a failed mission, a follow-up on a merged one), takes the project's write lease again first, and holds with the holder named if a sibling has it.

The issue lifecycle sweep also remembers the conclusion: it will not relaunch a mission for an issue whose most recent Orgabot round concluded "already satisfied" (confirmed or not) until you edit or reopen that issue after the round ran. GitHub bumps the issue's last-updated time on either, which is exactly the signal the sweep watches for. Until then, relaunching (the dashboard's Fix button, or a "Not complete" note) is refused rather than spending another worker session to reach the same conclusion; the refusal is recorded in the sweep's decision log alongside every other launch/hold/skip decision.

The mission terminal shows exactly one current summary per session, as the structured Mission Outcome block described in The mission terminal, and while the mission stays settled it is the last thing the session shows. The block is updated in place as the mission settles, so a mission that parks and then ships does not leave two summaries stacked on top of each other. The terse lifecycle row that restates the same settled status is absorbed into that block whether it reaches the terminal before or after it, so the summary never renders once and then again a few seconds later in a different colour.

Everything else that restates the same ending is absorbed into that one block too:

  • the greppable MISSION OUTCOME: … line, which is the record's status and summary in one line;
  • any log line whose sentence the block already carries (the CLI prints the summary itself on several terminal paths); and
  • the hold reason in the yellow waiting block pinned above the prompt: that block keeps only the control that answers the hold, and disappears entirely when it has no control to offer.

Each of those is dropped only on evidence that the blue block genuinely states it: with no Outcome block on screen, or a hold whose reason never reached the mission record, the line stays where it is rather than vanishing from both places. Nothing is removed from the mission log itself, which the CLI tails and orgabot wait --json reads.

The steps that are still yours

A mission can finish green and still be inert until a human does something: set an environment variable, paste a key into a hosting console, submit a URL to Search Console. Orgabot reads those steps out of the mission's own report (the worker is asked to end with an ## Operator follow-up section) and records them on the mission.

They are stated in the Outcome block itself, as its numbered Next steps section, ahead of any step the worker's own report listed under a heading of its own. You do not have to scroll the log to find them. The same list appears in the mission drill-down under a Your next steps heading, in the Needs You inbox, and on orgabot status, orgabot mission list, and orgabot wait (including --json, as followUps).

They are quoted, never run: Orgabot states what the report said a human must do and takes no action on it. A mission that declared nothing shows nothing, so the heading only ever appears when there is genuinely something for you to do.

The block stays pinned to the foot of the scrollback for as long as the mission stays settled. Output keeps arriving after a mission ends (the closing banner and the MISSION OUTCOME: line are written to the log after the record is marked), and those lines used to append below the block and push the summary out of view. They now land above it, so the blue Outcome panel, with the status named in its heading, is what you are left looking at. It is never dropped by the scrollback cap, and a mission the dashboard's live snapshot no longer carries still states its outcome, read from the session's own journal. Once you send a follow-up instruction the old block is marked superseded where it stands, its action buttons are withdrawn, and that round's output appends after it; when the round settles, its own Outcome block lands below as the new current statement, so the history of what the mission said stays readable in order.

Recovery

  • A process that dies still says why. If a mission's process ends before its round settles (an uncaught error, an unhandled rejection, a closed output pipe, SIGTERM, SIGINT or SIGHUP, or an unexpected exit), the mission log gets a died: line naming the cause, followed by a MISSION OUTCOME: NOT COMPLETE line, and the exit is recorded for the round. A log that ends on a progress line with neither means the process was killed outright (for example SIGKILL or the machine losing power).
  • Waiting on a dependency is announced. When a stage has to wait for a dependency such as Restate, the log says so (⏸ waiting for restate …) and names how long the round waits before it holds the mission. A quiet log after a step does not mean the process died there.
  • Salvage. On worker death or timeout, work in progress is committed to a salvage branch, excluding untracked build-artifact directories and untracked files over 5 MB. Continue with orgabot mission --base <salvage-branch>.
  • `orgabot recover [--dry-run]`. Sweeps the store for shippable or dead missions and salvages them.
  • Auto-remediation. orgabot remediate run-due | <mission-id> retries retryable failures. It is a cron entrypoint; there is no daemon.
  • Interrupted-process recovery. A mission whose host process died is detected and its evidence preserved rather than silently marked complete. What happens next is same-id resume, below.
  • Launch failures. A child that exits before it creates work (for example, a missing Node preload, module, or binary) is marked launch failed, not interrupted. It appears in the Alert Bell for an operator to repair the runner environment and retry manually; interruption recovery never relaunches it automatically.
  • Delivery merge reconciliation. A mission held with its own pull request already open (its review loop ended without merging, a fix round's evidence had not yet landed on the journal, or the process exited right after delivery evidence was recorded) is settled to completed once that same PR is observed merged, by any route. Only the hold codes a merge can actually answer are settled this way; a hold about unmet issue scope, a rejected workflow gate outside delivery, an unavailable dependency, or an outstanding approval or input is left exactly as it was. A mission that stays held this way still has the merge recorded on it, which releases the per-project write lease it was holding: the lease exists to serialize writes that have not reached the base branch, so once the pull request is merged there is nothing left to serialize, and any sibling mission queued behind "the holder's merge" can start. The hold itself is untouched. See Mission outcomes for the detail.

Held missions are re-read against their source

A held (waiting) mission used to stay held forever once its round parked, even after the issue it was launched for was closed. The recovery sweep (the headless supervisor, or orgabot recover sweep) now re-reads the source of every held mission, live, and acts only on what it observes:

Held missionWhat the sweep observesWhat happens
Launched for an issueClosed as completed, and a merged pull request (not the mission's own) closes or references it, and the mission has no open pull request of its ownEnded as resolved elsewhere, citing the issue and the pull request
Launched for an issueClosed as completed with no merged pull request, or closed as not plannedKept held, marked stale
Launched for an issueIts own pull request is still open while the issue was resolved elsewhereKept held, marked stale; close or merge the pull request, then stop the mission
Launched for an issueOpen, or unreadableKept held
A child of an epic whose coordinator was cancelledNo pull request of its own, or that pull request was closed unmergedEnded: nothing will ever drive it again
An epic coordinatorThe epic is closed as completed, and no child mission is running or queuedEnded as resolved
An epic coordinatorThe epic is open, unreadable, closed as not planned, or a child is still liveKept held

A mission whose own pull request merged is left to delivery merge reconciliation and acceptance re-inspection, and a mission that belongs to an epic still under supervision is left to the epic's own node settlement, so each question has exactly one place that answers it. A hold that only you can clear (an approval, a capability or identity grant, an answer) meets the same strict bar: it is ended only when the issue is closed as completed with a merged pull request that is not the mission's own. Ending performs no action and grants nothing: a pending approval is denied, never approved, and a pending question is withdrawn.

"Ended" is a stop, recorded on the lifecycle journal as held_source_resolved with the evidence on the mission. It is deliberately not completed: nothing of the mission's own was delivered, and completing it would record verification it never ran. The branch and worktree are kept, exactly as orgabot stop keeps them. A mission marked stale keeps its hold; the mark only records what was observed, so orgabot stop <mission-id> is yours to run when the work is done. The sweep spends GitHub reads under its own rate window, and a mission it read is not read again for an hour.

Session-limit death is a failure, not a success. A Claude Code session or usage-limit message in the worker stream maps to a distinct worker failure. It must never surface as a quiet "completed / no changes produced".

Same-id resume

Recovery continues the mission, not the process that was running it. When a mission loses its owner (you kill the controller, the stack restarts, the host reboots), Orgabot starts a new owner for the same mission id, on the same branch, at the round its lifecycle journal says is current. The mission id you were watching does not change, orgabot status and orgabot mission show keep showing the same mission, and the dashboard card stays where it was.

A few consequences worth knowing:

  • A missing process id never changes a mission's status. Whether a mission has a live owner is read from its journal: a process counts as live only while it is heartbeating. A dead process id is a hint that nobody is driving the mission, and the response is to start someone, not to fail it.
  • No successor mission is minted. There is no second mission id, no second branch, and no second row in your organization's mission list. (Your explicit Retry is unchanged and still starts a fresh mission linked back to the old one: that is a decision to start over, and it stays yours.)
  • A missing worktree is rebuilt from the branch. Orgabot pushes the mission branch after every commit, so a checkout lost to a reboot or a cleared temp directory is recreated from what was pushed and the round carries on.
  • An epic controller has no branch, and resumes anyway. A mission that coordinates an epic writes no code of its own, so it records no branch; what it continues from is its saved work graph. If its process stops, recovery relaunches the controller under the same mission id and it picks the graph back up, including any child launch that was deferred behind another mission's write lease. Two guards keep that relaunch honest. A controller that launched less than a minute ago and has not yet registered its process on the journal is still starting up, not orphaned, so no recovery pass relaunches it inside that window. And a controller whose record names no owning organization and role is refused rather than relaunched: the refusal names the missing fields in the mission log, because an epic controller is owned by a role and relaunching it without one only produces a second controller that holds on epic_role_unresolved. If a controller does reach that hold, it is not a dead end: the controller inherits the role its launch resolved (recorded on its own mission), and when nothing on the record resolves it raises a role-assignment question against the epic mission, so orgabot inputs lists it and orgabot answer <id> "<role>" relaunches the same mission under the role you name.
  • Only a branch that is gone everywhere degrades to a restart. If the branch exists neither locally nor on the remote there is nothing to continue from, so recovery restarts the objective on a fresh branch and records recovery.degraded_to_restart, with the reason, on the mission's journal. You can see it in orgabot timeline <mission-id>.
  • A held mission is never resumed automatically. A mission waiting on you (an approval, an answer, a capability grant) is at rest on purpose. Nothing about a dead process changes that; only you release it. Two passes may still settle one, and neither resumes work: delivery merge reconciliation, above, settles a mission whose own delivery pull request is observed merged, and only for a hold that question can actually answer; and the held-source pass ends a held mission whose source is observed resolved.
  • Relaunching is bounded by the clock and by evidence, not by a count of attempts. A relaunched owner is given a few minutes to produce something on the journal. As long as the mission keeps producing events it keeps going, however many rounds that takes; a streak of relaunches that produces nothing at all stops after half an hour and leaves the mission visible for you, rather than restarting forever.

Durable missions

Foreground missions prefer Restate when the durable runtime is up. The workflow journals worktree → worker → commit → verify → review → PR, and on crash it resumes from the last completed step: the worker is not re-invoked and its cost is not re-charged. It can suspend indefinitely waiting on a human approval signal.

If Restate is not running, the mission continues on the ordinary in-process path with no error and no setup prompt. You do not configure this per mission.

--durable is the strict form: require Restate and fail if it is unavailable (useful when you need guaranteed crash resilience). Background missions keep their detach model unless you pass --durable.

Restate is downloaded on demand via npx; there is nothing to install manually. It was chosen over Temporal by a project decision that overrides the original PRD.

Start it with the whole stack rather than by hand:

orgabot up                 # dashboard + Restate + Mission service + registration + GitHub

See starting the stack. The multi-terminal form below still works and is what orgabot up supervises for you; reach for it only when you are debugging one component:

npm run restate:server     # terminal A - ingress :8080, admin :9070
npm run restate:service    # terminal B - Orgabot Mission workflow :9080
npm run restate:register   # once after both are up

Because a missing runtime is a silent fallback rather than an error, a forgotten stack looks exactly like everything working while crash resilience is off. orgabot doctor and orgabot stack status both report which components are up, for that reason.

Pending work

~/.orgabot/pending.json tracks work that is built but undelivered. A record is deleted only on a first-hand, repo-scoped fact: its PR observed merged (by any route, not just Orgabot's own ship path), or its branch existing neither locally nor on the remote.

orgabot pending list
orgabot pending reap [--dry-run]

Uncertainty keeps a record. An unreadable source is not evidence of absence, and a cached branch list read before a record was written says nothing about that record.

Overlapping epic children

Epic planning orders children whose own work-item text names the same source files. Existing dependencies take precedence, including dependencies discovered in the opposite order. Children with independent declared files remain eligible to run in parallel. Mission parent edges use one shared validator; re-launching a child preserves its recorded parent instead of assigning the replacement coordinator as a new parent.