Skip to content

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

← All documentation
Using Orgabot

Missions in depth

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

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.

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 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 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

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

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.

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 cancelled 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.

If you answer after the mission has already ended, orgabot answer (and the dashboard) say so rather than reporting a resume that did not happen. When that mission left verified and reviewed work behind, deliver it with orgabot ship.

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.

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>.

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.

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, 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 still holds as waiting, same as today.

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

  • 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.
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.
  • 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. The one exception is delivery merge reconciliation, above: it never resumes work, it only settles a mission whose own delivery pull request is observed merged, and only for a hold that question can actually answer.
  • 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.