You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Each reviewer is told the tools it really has (ADR 0016). On the second 0.3.1 retest every demo goal ended EXHAUSTED: a packet-only approval started the tree's fresh review, and the Codex reviewer, told by the shared role prompt "You have no shell; use Read, Grep and Glob" (tools only the Claude CLI has), looked for them through MCP tool discovery, opened nothing, and blocked with a finding no repair could answer. That prompt is also why Codex reviews never made a tool call. The role prompt now names no tools; the review task names the provider's own (read-only shell commands for Codex, Read, Grep and Glob for Claude); and a Codex review's depth counts only completed commands, so tool discovery is not an inspection and such a review blocks the run with "the review inspected nothing" instead of looping through repair.
The ui fail-before probe installs dependencies in its control and probe checkouts as the command probe does for its probe tree (ADR 0016). On the second 0.3.1 retest the demo's control ran npx playwright in a fresh base checkout with no node_modules, the sandbox refused the download (E403), and the probe recorded that the journeys do not pass on the base revision. A run whose output or application log shows a dependency could not be fetched or installed is now an environment cause, never a journey failure or a failing base, and is not cached; every install attempt's log is read for the same failure, for the command probe too, so an install behind a shell pipeline that exits 0 with an incomplete install stops the probe.
The fail-before probe decides statements about fail-before (ADR 0016). On the 0.3.1 retest the planner wrote AC-3 "The new tests import round_price and so fail on the base revision", marked supported by the candidate's passing test log, and AS-3 "The unit-tests check runs pytest over tests/ with src on the path", left unverified at success, although its brief forbids both. Such a criterion is now supported only when its fail-before outcome is failed-before; such an assumption only when each check it names (or every probed check, when it names none) failed before, and one naming a check that is not configured stays unverified. Unverified, it blocks under required and is a residual risk under advisory or off. Wording only selects these statements, and only to make Orbit stricter: negated statements, quoted or dotted check names, the word orders the reviews found and one strict passive shape ("the pytest suite is run by the unit-tests check", which the third 0.3.1 retest's planner wrote) are handled; a phrasing outside that grammar keeps the planner's own status, as before (ADR 0016, Limits). The evidence report records the assumptions it judged, so a changed assumption makes it stale, and the final report shows a probe-decided status only for the same statement.
Browser journeys get fail-before evidence of their own (ADR 0016). The probe ran only command checks of category test, so the demo's ui criteria read "ui is not probed". A journey check whose directory holds a journey the candidate adds or changes is now probed through the same ui runner and app fixture as verification, after a control run of the journeys on the base revision alone (once per run, since the baseline runs command checks only). A failure counts only after a passing control; a skipped journey is not a failure; a browser that never started, a process that never reached the repository's code, or an application Orbit did not start from the probe's checkout (no ui.environment.start_command) establishes nothing; and one deadline covers the control and every attempt.
A review that inspected nothing no longer clears the tree (ADR 0016). On the 0.3.1 retest every Codex review approved from the packet alone, with no tool call and a few hundred reasoning tokens, and the report could only flag it ("NO TOOL CALLS"). An APPROVE whose recorded depth shows no tool call now starts the tree's one fresh review at high effort, told to open the changed files and tests before its verdict; an approval from that review clears the tree, and if it inspects nothing either, or review_rounds is spent, the run blocks with the reason ("the review inspected nothing") and orbit status says a new run is needed. A review whose depth was not recorded is unaffected. The fake Claude and Codex CLIs in the test suite now look at the repository once per session, as a real reviewer does, unless a step says inspect: false.
The implementer works faster on what the 0.3.1 retest showed (ADR 0016). The demo's ui goal took 963 s, 11 minutes of it in one implementer session that ran at xhigh on predictions alone (a complex classification and a planned coupled change), hit its 30-turn cap and spent turns on refused commands. A prediction now raises an implementer's effort at most to high; repeated equivalent failures or critical security impact still raise it further. The implementer brief names the commands that were refused: inline code (node -e, python -c), which the session refuses even inside the sandbox (the retest's npm test 2>&1 | grep was refused for the node -e in the same call, not for the pipe), and reads outside the worktree. And usage.output-budget-exceeded now judges the final structured output, estimated from its JSON, against the budget it instructs: the session's 45,643 output tokens, every file it wrote included, were reported over a 24,000 budget the final output never came near.
The way to run without delivery, when a delivering run has no GH_TOKEN or no gh, now names every setting to change. On the 0.3.1 retest the demo's policy (autonomous-delivery, every delivery action on) was refused with "or set mode: autonomous to run without delivery", and following that alone was refused again: a delivery action left on is invalid in a mode that does not deliver. orbit run, orbit doctor and the controller's preflight now say "or, to run without delivery, set mode: autonomous and actions.commit, actions.push_task_branch, actions.open_pull_request and actions.repair_ci to false in .orbit/config.yaml", naming only the actions that are on, and the demo notes say the same.
orbit doctor no longer fails a check whose executable the dependency install creates. On the 0.3.1 retest the Python journey's check ran .venv/bin/python, which dependencies.install_command (python3 -m venv .venv ...) creates in each run's checkout, and every run's checks passed, but doctor reported "1 of 1 check(s) cannot run" because the file was not in the source checkout. A relative executable inside the repository, with an install command set, is now a warning that says the install may create it and that a run blocks at its baseline if it does not; doctor does not run the install. A bare name, an absolute path or a path outside the repository still fails, as does any check with no install command.
Orbit no longer sends AppleScript notifications on macOS (ADR 0008 addendum). The desktop channel is skipped there, with a detail that points to the webhook channel; the notifications appeared as Script Editor's. Linux notify-send and the notifications.desktop setting are unchanged.
A review finding a repair addressed is resolved by the repair's test only when the finding's own test tells the repaired tree from the one before it (ADR 0015). On the 0.3.0 retest a high test-quality finding (TEST-1, "the fail-before evidence is only a load failure") was marked resolved with "a test exercising the claim passes on tree ... (new_test lint PASSED ...)": the resolution took the first required check that passed on the repaired tree, which was lint. Crediting instead a test check whose log named the repair's test as passing was broken by three adversarial reviews (a check credited with a test its command excludes, a skipped test counted as run, a title matched inside another test's title, a sibling test's pass credited to a skipped one, glyph-only skip marks, a compiler warning that names the file), and Orbit does not read test runner output, so that rule is gone. Now each required command check of category test that passed on the repaired tree, whose directory holds a test the repair named for the finding, and whose latest run on the pre-repair candidate passed without a rerun, runs with the fail-before probe's machinery (ADR 0014) on three trees in one probe checkout: two built from the pre-repair candidate's tree, production code unchanged (a control tree with the repair's test-owned files in their repaired state except every test named for a finding, and the finding's tree, the control tree with the finding's tests), and the repaired tree itself; a failing run is repeated once, within verification.fail_before_timeout_seconds for each tree, whatever verification.fail_before says. The finding is resolved only when such a check passed on the control tree and on the repaired tree in the probe checkout, failed on the finding's tree on every attempt, with no environment signal and its logs read whole, and passed on the repaired candidate: exit statuses alone decide. A fourth adversarial review had a first version of this rule, one probe tree with all of the repair's test files, credit a skipped test named for one finding with another finding's failing test, with an unnamed edit to an existing test, with a test only a second check runs, and with a pre-repair tree that failed the check by itself; each now leaves the finding open, with "unit fails on the pre-repair tree without the finding's tests too, so its failure cannot be tied to them" or "unit is not probed: it did not pass cleanly on the pre-repair tree". A skipped test, a test the check's command does not run and a test that does not check the fix pass on both trees, so the finding stays open and blocking with "the repair's test passes on the pre-repair tree too, so it does not tell the trees apart"; a timeout, an ERROR, an environment signal, a log that cannot be read whole, the probe's bound or a repair that changes a dependency manifest or lockfile leave it open with that cause. A fifth review had a test file named for two findings credit a skipped test with the other finding's failing test in the same file, and a test that failed only for the probe checkout's path resolve a finding: a file named for more than one finding now resolves none of them ("is named for TEST-2 too, so a failure in it cannot be tied to this finding"), and the repaired tree runs in the probe checkout, so such a test fails there too ("did not pass on the repaired tree in the probe checkout"). As in ADR 0014, a test that fails to load on the pre-repair tree (it imports a symbol the repair adds) counts as failing, and a test that reads the probe commits' git metadata to fail only on the finding's tree is not caught: the review is the defence against both. A test is named for a finding by its id or fingerprint as a whole word, so a test named for TEST-10 no longer resolves TEST-1, and an id shared by findings of different reviews names the one whose fingerprint the reports cite for that file. A check of category lint, typecheck or build never resolves a finding, and a reviewer who raises the finding again on the repaired tree, or on a later tree that has the test, keeps it open: before, the findings of both reviews read resolved, and a test a review had held insufficient resolved the finding on a later tree whose repair named no test. A resolved finding names the attempt whose test resolved it. The repair probe's attempts are recorded like the fail-before probe's, never in check_runs or failures, with logs under evidence/<n>/repair-probe/ and one review.repair-probe decision per probe tree. The repair brief says what resolves the finding.
A run no longer ends SUCCEEDED while a needs-decision entry of its assumption ledger is open. Only a person may settle such an entry (a choice the Inquisition would not make on its own), yet on the 0.3.0 retest a Python advisory run recorded three in two inquiries, asked nobody, and succeeded. Before delivery, as CONTRACTING does for a needs-decision assumption, each open entry now goes to a person: an entry an inquiry derived from its trigger while it put questions to a person is settled by a person's answer to one of them (the inquiry records which entries its questions stood for, as an inquisition.ledger-asked decision); every other open entry is named in one material question ("Accept the decisions this run recorded without a person: as-..., as-...?", with each entry's claim in its evidence), and the run blocks on it. accept settles each named entry as supported on that answer's decision record, and orbit resume delivers; decline (or any other answer) keeps the run from success: orbit decide then says to cancel the run and start a new run whose goal states the decision, and a resumed run blocks again with a reason that begins "a person declined ..." and records the question and its entries in the outcome's new ledger_declined field, so the CLI's closing line, orbit status ("a new run is needed: a person declined ..."), the report's next action and a notification all say the same instead of "resume". Nothing more is asked once a person has declined. The completion gate names any entry still open.
The demo app's checks say what they evidence (lint: category lint, unit: category test, ui: category ui), so the fail-before probe no longer runs lint as a test. A check without a category is a test, and the demo's lint check had none: on the 0.3.0 retest the simple run probed it ("lint passed on the base revision with the new tests: AC-2"). docs/configuration.md documents the category key in the checks table and says that a check without a category is treated as test and is probed; the starter template's examples give every check a category. orbit init already writes the category of each check it proposes, from the tool that proposed it (lint, typecheck, build or test); nothing new is detected.
The evidence report says when a criterion's evidence is check-wide. Evidence granularity is the check (ADR 0014 section 4): a criterion is supported, and its fail-before outcome decided, by the whole run of each check it cites, which other criteria may cite as well. On the 0.3.0 retest the demo's "passes lint and the whole unit suite" (AC-2) and "existing behaviour unchanged" (AC-9) criteria read failed-before because the new tests in their unit suite failed on the base revision, and UI criteria cited every journey's artifacts, with nothing saying why. A criterion citing a check that another criterion cites now carries check_wide in report.json ("check-wide: the tests unit runs, shared by AC-1, AC-2"; the journeys of a browser check, what a lint or build check checks), and so does its fail_before entry when a probed check is shared. Existing fields are unchanged.
The final report carries each criterion's fail-before outcome. ADR 0014 says a criterion's report entry carries it, but only evidence/<seq>/report.json did: orbit report showed "[supported]" alone, and the criteria of orbit report --json and final.json had only id, statement, mandatory, status and artifacts. They now also carry fail_before (mode, outcome, cause when it is not established, reason, and check_wide when its check is shared) and the criterion's check_wide, and each line under "Criterion evidence" in orbit report and final.md ends with the outcome, for example "fail-before (advisory): failed-before: unit failed on the base revision with the new tests (exit 1, 2 attempts); check-wide: the tests unit runs, shared by AC-1, AC-2". A criterion without a fail-before entry reads as before.
Fail-before under advisory and off no longer reaches the reviewer or the Inquisition (ADR 0014 section 1: no status, verdict, repair or question changes because of it). In the 0.3.0 retest the advisory review packet carried a Fail-before section and the reviewer brief told every reviewer to judge the probe log, so a reviewer raised "the fail-before evidence only shows a load failure" as a high finding: two repairs and two amendments of a criterion's proof in a Python run, two REPAIR_REQUIRED rounds in the demo UI run. Under advisory or off the packet now has no fail-before section and no fail-before line among what is not verified, and the reviewer brief's fail-before paragraph applies only under required. What the packet says instead, after the criteria, is that whether the new tests fail on the base revision is Orbit's to establish, so a criterion's promise of it is not a finding (see the planner entry below). The report still carries the outcome for the person.
Under verification.fail_before: required, a compile, collection or import failure of a new symbol counts as fail-before and is not a review finding. The packet and the reviewer brief told the reviewer to report a load-only failure as a blocking high finding; every test of a new function fails on the base revision that way, so the canonical test-first goal ended EXHAUSTED in Python and .NET. The reviewer is now told the outcome is Orbit's and is asked only about the new tests' quality (a tautology, an assertion that cannot fail with any implementation), never for mutant runs, stub implementations or a probe rerun, which no check produces.
A review finding that only disputes a classification Orbit made of its own evidence (today the fail-before outcome, such as "the fail-before evidence shows only a load failure") is reported as a note in every mode: an advisory finding with a review.finding.noted decision that never blocks, is never a claim to test, and goes to neither repair nor the Inquisition. A repair changes the candidate, not the base revision, so in two .NET runs the repair reproduced the same tree and the run ended EXHAUSTED on non-progress. Whether a finding is one is structure, not wording: the review output has a finding category evidence-classification, which the reviewer brief reserves for a finding that disputes Orbit's recorded classification, or a criterion's promise of one, and asks for nothing a change to the candidate can give (a finding about the tests' content, coverage or assertions or the code's behaviour, or one that mixes both, uses its normal category), and a finding is a note if and only if its category is evidence-classification, no check confirms it (no FAILED evidence, no earlier acceptance) and no report of it has a security id (SEC-): an id that says security and a category that says the candidate need not change contradict each other, and the structure resolves that toward the stricter security rule. Any other category, one an older reviewer writes included, is an ordinary finding whatever its claim says. An earlier version of this change recognised a note from the words of its claim, clause by clause; three rounds of adversarial review each found a claim naming a real defect that it still silenced, so the resolver no longer reads a claim's wording to decide it. Delivery still needs an approving review, and a review that asked for repair only through such notes approves nothing. Instead of an attempt spent on the same tree, that tree gets exactly one fresh review (a review.fresh decision, charged to review_rounds like any review), whose prompt names the notes and says the recorded classification is not a finding and cannot be repaired by changing the candidate; the earlier review stays on the record, superseded. The fresh review is an ordinary review: its APPROVE lets the run go on, and a defect of the candidate or a claim to test it reports goes to repair or the Inquisition as any review's would. If it again asks for repair only through such notes, or no review round is left to start it, the run ends BLOCKED naming the findings (noted_findings and fresh_review in the outcome). A resume would decide the tree the same way and start no second fresh review (and the cap is the run's frozen policy), so orbit status, the report's next action, the CLI's closing line and the notification say to cancel the run and start a new one instead of to resume it.
The planner no longer writes a criterion that the new tests fail on the base revision, or an assumption that a check runs the new tests: both are Orbit's to establish (verification.fail_before). A Python run's planner wrote "The new tests fail on the base revision (ImportError for round_price)", which the candidate's passing log was then cited to support, and an inquiry told the implementer to keep the ImportError "as the contract states". The planner brief says so, and the Inquisitor brief no longer lets an amendment require evidence no check produces, nor any fail-before mechanics (a mutant run, a stub implementation, keeping an import error "as the contract states"), since Orbit's probe establishes fail-before. Orbit never removes a criterion: an earlier version of this change dropped one from the contract draft by its wording, and adversarial review found real requirements it deleted ("the new tests fail on the base revision and authentication rejects cross-tenant requests while valid requests pass"), so a criterion the planner writes all the same stays as written. Since such a criterion reaches the reviewer, the reviewer brief, the packet under advisory and off and the fresh review's prompt say, whatever the criteria say, that whether the new tests fail on the base revision is Orbit's to establish through verification.fail_before: a criterion's promise of it is not a finding against the candidate and does not withhold APPROVE, its other clauses are judged as usual, and a finding that disputes only that promise uses evidence-classification. Under advisory, the retest's AC-4 reached the reviewer as a mandatory criterion backed only by the candidate's passing log, and a finding that it lacked evidence would have blocked as a claim the candidate cannot answer.
Under required, the review packet shows the tests that failed on the base revision instead of the last 2,000 characters of the probe log, which for a test runner are its passing suites and summary. The excerpt is built from the lines that report a failure, in any runner's words, with a little context, within 4,000 characters per log, falling back to the log's head and tail, and the packet says the log is check-wide.
ADR 0014 has a dated addendum (2026-10-13, 0.3.1) recording these changes and why, with the superseded sentence of section 4 marked; docs/configuration.md and docs/operations.md describe what the reviewer sees in each mode, and the one fresh review a tree gets when its review asked for repair only through notes.
A base-revision check whose program the sandbox does not let it start (exit 126, "Operation not permitted" for a program the command starts: its own, any command's of a shell chain or pipeline, one exec starts, one a wrapper starts (timeout 600 pytest, nice, nohup, stdbuf, xargs, time, command), or one of the script a shell it starts with -c runs (after -- or bash's long options too), by its absolute path as the command writes it, spaces included, or, for a bare name, the path the shell found it at on PATH, as the shell, env or Orbit's limits wrapper reports it) is now an environment failure at PREFLIGHT: the run blocks there with the fix and is never offered a baseline exception. A program in the repository or in the folder that holds it and its sibling projects, both of which the check sandbox denies, is named with those folders. On Linux, where srt hides a denied folder, the interpreter of /abs/venv/bin/python -m pytest that is not found (exit 127) is read the same way, since the program a command starts now counts as well as the module it runs, and so is one a chain starts by its absolute path; that reading follows bash 5's message under Orbit's limits wrapper and was not measured on a Linux host. Before, a check pointed at a virtualenv in the repository or beside it passed PREFLIGHT "with 1 pre-existing failure", raised a baseline-exception question and spent a contract and an attempt before it blocked (0.3.0 retest), and a chain such as cd tests && /abs/venv/bin/python -m pytest, a script that execs it, or a path with a space still did after the first fix (its review). orbit doctor and PREFLIGHT now read the programs of a check's command with one function, so they name the same program. Whatever the command looks like (eval "$CMD", make test, a script of the repository), PREFLIGHT also reads the output alone: a launcher's refusal of a program in those folders (a shell's, a wrapper's, make's, a script's own ./x.sh: line 2:, node's spawn, uv's two-line Failed to spawn), by its path, a bare name the check's PATH finds there, or a link that leads there (from outside the projects folder, or one the repository commits and the command runs by a relative path, read in the base revision's tree (never in the working repository, whose uncommitted link under repository.allow_dirty_start the baseline never ran) from the directory the failing command ran in, a chain's cd tracked through command cd and builtin cd too, and not read at all where that directory is not known for certain (a pushd, an option that is not recognised, a cd to a variable or to a substitution), when the link's target is an absolute path there, even where the file there links on elsewhere, or a relative one that leads out of the repository; a relative link that stays in the repository moves with the checkout), any other "Operation not permitted" for a path there, or a "not found" for such a program (a launcher's whatever exit code it passes on, as make's 2 and turbo's 1, and any with exit 127 or make's Error 127 for a path there), is the same environment failure, unless the check's tests ran and failed (ctest's failure report and a formatter's verdict count as that too); a line of a unified diff a comparison prints (git diff --no-index) is never read as one. Such a program not found or refused (exit 127 or 126) is an environment failure even where the shell's words also read as a script that does not exist yet (a relative link to a sibling virtualenv was a missing target). Only launch-specific evidence counts: a launcher's own refusal or not-found line, a spawn report, or a shell's exit 126 or 127 for the program. A language runtime's file error is never read as one (Python's FileNotFoundError for a file the check opened, as in python3 -c 'open("tsc")', names no program it tried to start, whatever PATH holds one of that name in those folders), so such a failure stays a baseline-exception question. Recognising a base-revision check that could not start its program is best-effort over shell command shapes and messages: a case it misses is offered to the person as a baseline-exception question that shows the failing log, never accepted silently.
A program the check's command runs that is not found is read in the words of Orbit's limits wrapper, which every check runs under by default: bash 5's orbit-limits: line 1: <path>: No such file or directory and orbit-limits: line 1: exec: <name>: not found (exit 127), and macOS bash 3.2's orbit-limits: line 0: exec: <name>: not found (exit 127) and ... cannot execute: No such file or directory for a path (exit 126). So are GNU coreutils' words for a program a wrapper could not start (timeout: failed to run command 'pytest': No such file or directory, exit 127, or Operation not permitted, exit 126). A bare name any command of the check starts that is not found (exit 127) counts too (cd tests && pytest -q, timeout 600 pytest), not only a direct invocation's, and so is bash's bad interpreter: Operation not permitted for a script the command starts (a virtualenv's pytest); a name only a script of the repository runs does not. Before, such an argv check was filed as a pre-existing failure and offered a baseline exception (a shell check, whose own shell prints the line, was not affected), and so was a chain whose bare name srt hid.
A check that runs a bare name (pytest -q) whose PATH finds it in the repository or in a folder beside it, as when a tool environment there is activated in the shell Orbit runs from, blocks at PREFLIGHT naming the program's path, the PATH it came from and the folders the sandbox denies (under srt 0.0.78 on macOS the name is not found, exit 127; a shell that does find it is refused, exit 126). When that PATH is the one Orbit runs its checks with, the fix may be a shell without the tool environment, so the reason keeps orbit resume and records no policy_fix; when the check's env sets it, the fix changes checks.<id>.env and needs a new run. Before, the reason did not say why, and said only a new run with a changed checks.<id>.command would help.
orbit doctor (checks.sandbox) warns about a check whose program, its argv's or one its shell command starts (a nested sh -c script's included), is an absolute path in the repository or in the folder beside it, whatever the tool, before any probe, with the fix: build the tool environment inside each checkout through dependencies.install_command, or place it outside the repository's parent folder. A probe the sandbox refuses to start (exit 126, "Operation not permitted") is reported as refused, not as an exit with no denial. docs/configuration.md ("Tool environments") says where a tool environment such as a virtualenv may live. Doctor reported 0 failures for both layouts in the 0.3.0 retest. Doctor reads the command's text alone, which cannot show for certain that a run reaches the program, so it never fails a check for such a program: a case pattern, a function never called, a [[ test, a ${...} or /usr/bin/true || <repository>/tool that names the repository failed a valid check. Its detail says when it cannot read the command for certain either (a command substitution, a here-document, eval, a case statement, a function, a [[ test, a parameter expansion, an array, shells nested more than three scripts deep) or the path holds no executable file. A command substitution is read as part of its word, so the argument after it is no program (echo $(git rev-parse HEAD) <repository> && npm test was refused). A program that links into those folders from elsewhere is named too, with the file it links to; PREFLIGHT blocks a run whose check could not start such a program.
orbit doctor's claude.plugins check no longer reports plugins of scope synced, which workers do not load (they start with no user, project or local settings): in the 0.3.0 retest it warned that workers may load six of them, with a fix that would have allowed them, while all 58 worker sessions loaded only Claude Code's built-ins. A session that does load a plugin the policy does not allow is still refused.
The environment-failure cause of a refused .NET named pipe names the process it belonged to: "an MSBuild worker node ..., so MSBuild built nothing" for a check the runner stopped for a refused node, "the build host dotnet format loads the project with ..., so nothing was formatted" for dotnet format's build host. A dotnet test check without -m:1 was told about dotnet format's build host, from a cause the two cases shared (0.3.0 retest).
A base-revision environment block whose fix changes a check's definition (-m:1 on its command, a dotnet format check removed or put in the form that loads no project, a program moved out of the repository) now ends "cancel this run (orbit cancel ) and start a new run with orbit run", and the CLI's closing line, orbit status and the report's next action say the same, never "resolve, then orbit resume": the run keeps the policy it started with, so a resume runs the same check again. The outcome records the settings as policy_fix. A fix outside the policy (a cache filled, a program installed) keeps the resume (0.3.0 retest), and so does a program outside the denied folders that only links into the repository's tool environment (such as /opt/tools/python into its virtualenv), whether the command names it or the check finds it on the PATH its env sets: moving the environment out of the repository's folders and repointing that link needs no change to the check's command or its PATH, so no policy_fix is recorded for it and the fix names that remedy and the resume. A network denial records no such setting: a pinned dependency restore may need only its cache filled outside the sandbox, so its fix names both remedies, filling that cache and resuming, or allowing the host in .orbit/config.yaml and starting a new run.
A base-revision block with a check that could not run and another that did not complete, and nothing misconfigured, names each for what it is. It began "Check is misconfigured" with no check named and ended "correct in .orbit/config.yaml"; it now ends with the environment fix's way forward, a new run when that fix changes a check.
Cancelling a run closes its open questions. A cancelled run kept its baseline exception question open, so orbit status still said "questions: 1 open" after orbit cancel; the questions are now withdrawn (question.withdrawn) when the run ends CANCELLED, whether by orbit cancel or by a cancellation that outranked another outcome. A BLOCKED run keeps its questions, which orbit resume waits on.
evidence/<attempt>/ui/ui-run.json is updated when the UI check ends. It stayed "state": "running" beside a ui-result.json that already had a verdict; it now reads finished with the verdict and endedAt, or aborted with the error when anything stopped the run after it started, a refusal or a failed read of the checkout after the journeys.
A run that ends EXHAUSTED or IMPOSSIBLE keeps its worktrees under ~/.orbit/worktrees, as before, and now says so: a worktrees.retained event names each one, and the report says the worktree and evidence are preserved and that orbit gc removes them after retention (the 0.3.0 retest found no record of them). Once orbit gc has removed them, a rebuilt report says so instead of listing them as kept. Releasing them when the run ended was tried in this release and dropped, because adversarial review kept finding ways it could lose work or remove a checkout beneath a running process. Wherever worktrees are released (SUCCEEDED, CANCELLED, and orbit gc after retention), before any implementer checkout goes, its unfinished work (tracked changes, untracked files the repository does not ignore, and a commit made in the checkout that no ref holds, which its detached HEAD alone kept) is pinned in a ref and recorded in a worktree.preserved event: the run's worktree as a candidate, and each unit-* worktree of a parallel attempt (found from the worker records) under refs/orbit/<run>/unsaved/<worker>, so a parallel attempt's unintegrated units are never deleted with their work; a tracked change counts as work even under a path the untracked-noise filter skips (node_modules/). A checkout whose work could not be pinned is kept, and so is a checkout a worker may still be writing in (stopping it failed, so its termination is not confirmed, in finishRun or in recovery) or a check run of the run is still open in (its detached shim may still run there): reconciliation releases it once a later controller start confirms the worker stopped or closes the check. A release that never completed (the controller died between the terminal transition and the release, or the release failed) or that kept a checkout is tried again at the next controller start, under the run's lease, and the run's report is written again after it; orbit gc releases a run the same way before it removes anything, pinning its unfinished work first, and skips a run whose release keeps a checkout or that has a worker still PLANNED or RUNNING. The report names every pinned ref, also when no release completed. Reconciliation itself no longer records a worker it stopped as CANCELLED, or gives up on a lost worker whose provider outlived its shim, before the process is confirmed gone: as stopWorker already did, it leaves the row RUNNING for a later pass (recovery.stop-unconfirmed, or recovery.restart-refused with unverified-process). A shim that was launched but never wrote pid.json is stopped by its process group and confirmed gone before its worker is finished, restarted or recorded FAILED: by stopWorker (so finishRun and orbit cancel), by reconciliation of a starting or lost launch, by launchShim when the shim stays silent past its wait, and by a failed start; one that cannot be identified or confirmed stopped leaves its row PLANNED or RUNNING, so its checkout is kept. It is identified by the pid and start time launch.json records after the spawn, else by registration.json, which the shim writes (pid, start time, group, launch token) before it starts the provider or anything else that can change the checkout. A launch record without a pid is never read as no launch: launch.json records a random token before the spawn and the shim carries it on its command line (--launch-token), so when no process in the process table carries the token the spawn never happened and the worker is closed, and while one does (spawned, not registered: it is not signalled) or the table cannot be searched, the worker stays active and its checkout is kept (recovery.stop-unconfirmed, or recovery.restart-refused with unverified-process, with the reason in detail); a record without a token, or one that cannot be read, is matched by its worker directory on the shim command line. A registration.json that cannot be read, or that another launch wrote, no longer keeps a worker active for good: the token search decides, and another launch's registered shim and its group must be gone too. A failed rewrite of launch.json after the spawn no longer abandons the shim. Reconciliation also leaves a check run of an ended run, and a fail-before probe attempt, open, with a recovery.stop-unconfirmed event, while its shim's identity cannot be established (it was not signalled), while the process group of a check whose shim died cannot be identified or is not confirmed stopped (signalling failed, or the stop did not end it), while a process carries its token on a check shim command line before launch.json names it (the runner now passes the intent's token there, matched as a whole argument), and while a check whose shim registered and died before pid.json named its group may still run (a process carries its token in its environment), so the release keeps that checkout; the release, and the next fail-before probe, also keep the checkout of an open probe attempt. The check shim writes registration.json before it spawns the check, gives the check its token in the environment (ORBIT_CHECK_TOKEN), and stops the check when pid.json cannot be written after the spawn. The check runner keeps supervising when launch.json cannot be written after the spawn, and records a cancelled, timed-out or lost attempt, or one whose start it could not record, only once the shim and its check are confirmed gone: otherwise the row stays open, the sandbox wrapper is left in place, and the step fails with why. The process-table search reads /proc on Linux (no procps needed, as in slim container images) and runs ps under a UTF-8 locale on macOS, so a repository path with a non-ASCII character no longer hides a live shim. The worktrees.released event gains preserved (worktree, ref, commit, worker) and retained (worktree, path, cause, why) beside removed and kept, final.json gains worktrees, and the report's next action names every pinned ref and every kept checkout with why. BLOCKED still keeps them, because it may resume. A run the recovery budget ends EXHAUSTED (a lost worker that cannot be restarted, spent crash recoveries, a stuck step the watchdog cannot recover) never reaches finishRun, so reconciliation and the watchdog record its retained worktrees too (ReconcileOptions.orbitHome and WatchdogOptions.orbitHome, which the controller loop passes). Verification and the baseline no longer remove a check checkout while a check run or probe attempt in it is still open (a stop that could not be confirmed): they keep it with a verification.checkout-kept or baseline.checkout-kept event saying why. The docs say a BLOCKED or paused run keeps its worktrees until it is resumed to an end or cancelled; orbit gc never removes them.
A same-provider review is no longer recorded as "independent review cleared" or as "gate.independent_review: pass". The gate decision reads "independent_review gate: review approved but it was same-provider review, not independent: " when the review approved, and "same-provider review cleared the tree without an approval, not independent: " when it only cleared the tree (with independent: false and approved added to its data); a block by a same-provider review says "review does not clear tree", not "independent review does not clear" (whether a review was independent is decided from the reviews of the tree, so a same-provider BLOCK that clears nothing is not called independent either; with no review yet, the recorded reviewer selection decides), and the transition to delivery says the review was not independent and why, as final.md already did. An independent review keeps its wording.
One-line outcome, gate and finding text is cut at a word with an ellipsis, never mid-word (the retest's "any assertion about th"). The new clipAtWord helper replaces the fixed-length cuts in the outcome of a review stall, the repair brief, the gate reason for a blocking finding, the residual risks and the decision summary. The full text of every review finding stays in the report: final.json gains review_findings, and final.md a "Review findings" section.
A BLOCKED outcome that gives the same reason for several criteria states it once and lists the criteria ("AC-1, AC-2, AC-3, AC-4: the new tests pass on the base revision: ...") instead of repeating the sentence per criterion, for fail-before evidence that passed on the base revision and for a probe that was not established.
The implementer brief says which shell forms are allowed and which are denied. Allowed: a configured check command, a test runner on one file and read-only commands, and a pipeline or chain of such commands (policy judges each part and takes the most severe, so npm test 2>&1 | tail -5 and npm run lint && npm test run). Denied: a for or while loop, command substitution and any path computed at run time (rm -r "$D" is refused as destructive), a heredoc or redirect that writes a file, writes through git, package installs the policy does not allow, permission changes such as chmod, and scratch directories outside the allowed paths. It points to the Write or Edit tool and the configured check commands instead, so a worker stops spending turns on denied calls.
Review depth is recorded and shown. When a review session ends, Orbit reads its tool calls and reasoning tokens from the provider's transcript, with the parser of the provider's family (claude, claude-*, claude_* as Claude; codex, codex-*, codex_* as Codex, so a codex-review alias is not misread as a Claude transcript with no tool calls; any other provider's depth is unknown, never zero, and never flagged), and records a review.depth decision; the report's review table shows both (tool_calls and reasoning_tokens on reviews in final.json, null when the transcript was unavailable) and flags a review that made no tool calls as NO TOOL CALLS, also under residual risks. Only a look at something counts: a Claude worker's StructuredOutput call (how it hands back its answer) and a Codex plan update (todo_list) are not tool calls, so a review that read no code and only answered shows 0. The Codex reviewer's reasoning effort is now providers.codex.review_reasoning_effort (low, medium, high, xhigh or max; default medium, where a review ran at the CLI's own default and answered in 17 reasoning tokens with no tool call); a config that sets only reasoning_effort keeps that effort for reviews, and the key is rejected on a Claude provider. schemas/config.schema.json gains the key.
CI: the verify job's timeout-minutes is 45 (the macOS job reached 30 minutes 11 seconds after its last test on PR #76).
Inquisitor spend is charged to the cost budget like every other worker's. The Inquisition records an inquisitor's usage itself, and the controller charged a session only before recording its usage, so no inquisitor was ever charged: in every Python run of the 0.3.0 retest cost_usd fell short of the measured spend by exactly its inquisitors ($1.01 charged of $1.17, $1.31 of $1.51, $0.22 of $0.27), and many Inquisition rounds could spend past model_cost_usd unseen. The controller now charges each inquisitor's recorded usage when its inquiry settles (or fails), once, recorded as budget.usage-charged, by the rules every other session is charged by: one that never reached a model (failed at launch or at authentication, no tokens, no model output) costs a measured $0 (budget.cost-zero-no-model), one that crosses the cap ends the run EXHAUSTED with the charge kept, and an authentication failure still blocks on credentials when its charge reaches the cap. An inquisitor is also admitted against the budget before it starts and runs under its own --max-budget-usd (worker.spend-cap), so it can no longer overshoot by a whole session; with no budget left for it, none starts and the run ends EXHAUSTED saying why. An inquiry that returns to REVIEWING is admitted and charged in the closing phase, as reviewers are, so a $0.20 inquiry during review no longer ends the run on the reserve a reviewer of the same cost may use. Every charge marks the usage record it covers (budget.usage-charged, with the amount and whether it was the reported cost, an estimate or a ceiling) in the transaction that charges it, whichever path charged it (a session as it is collected, an inquisitor's recorded usage, PLANNING's precharge), and a marked record is never charged again. PLANNING's precharge commits its charges, their marks, the recovery attempts and budget.precharged together, so a controller that dies part way no longer charges them again when PLANNING resumes (a $0.40 inquisitor was charged $0.80). A session whose result was stored with no usage record, one reconciliation collected after a controller restart or the watchdog stopped, has its usage recorded from that result and charged once: before the Inquisition reuses it, at the start of the next step at the latest, and for a run that has ended (the curator runs after the report is written; reconciliation collects a terminal run's workers) when the controller next starts, with the report written again (a $0.75 inquisitor reconciliation collected on restart, whose result the engine reused, was never recorded or charged, nor was a curator a crash cut short). A session the controller collects or stops has its result, its usage and its charge committed together, the curator included, so a reviewer stopped as obsolete mid-run is charged before the next session is funded instead of leaving its $6 neither charged nor committed until the run ended. A worker stopped as the run ends is settled the same way: one with nothing to collect (its files gone) was finished with no usage record and never charged, while the same stop mid-run charged its session cap; it is now recorded unmeasured and charged that cap, as is one reconciliation stops for an ended run, and the spend line names it. A worker that never started is no session and costs nothing (a stop charged it a ceiling). A lost worker reconciliation restarts has what the lost attempt spent recorded with the restart (recovery.worker-restart names the record as usage_id) and charged before the next step, beside the restarted session's own usage (a $0.60 lost attempt and its $0.40 restart were charged $0.40). A lost inquisitor with nothing to collect is recorded unmeasured and charged its spend cap, at most the role ceiling, as any other lost session is. All of this applies to a run started on 0.3.1 or later, which records it when it is created (accounting in run.created). A run started before 0.3.1 keeps the accounting of the version that started it when a later version resumes it: a session is charged and then recorded, as 0.3.0 did, and nothing is marked, recorded from a stored result or settled at the run's end, nor is its precharge read again (marking what 0.3.0 had charged and settling the rest charged again a reviewer whose charge 0.3.0 had committed before it failed to record its usage: an EXHAUSTED run went from $1.20 to $1.70 when 0.3.1 wrote its report). Spend from before PLANNING creates the budget is still charged when PLANNING completes. A run that stops before then (the 0.3.0 dotnet run blocked at PLANNING spent $0.06 with "nothing charged") now says its spend is measured and not charged, and why, in a new uncharged_reason: blocked before PLANNING created the budget (charged if the run is resumed), ended before then (never charged) or not there yet. orbit status no longer says "not initialized yet (the run has not reached planning)" for a run blocked at PLANNING: it says "none: the run blocked before PLANNING created them; PLANNING creates them if the run is resumed", and for a run that ended before then, "none: the run ended before PLANNING created them".
worker_turns_per_session reports the cap it enforces. The 0.3.0 demo's UI implementer "used 61 turns against a hard cap of 30" with no event. It was one session, launched with --max-turns 30 and never resumed, and it made 30 model requests, the last its structured output: the CLI enforced the cap. Orbit recorded the result's num_turns, which counts the prompt and every tool result, so the session's 60 tool calls, many in parallel, made 61. A session's turns are now its model requests, counted from its transcript (modelRequests in the worker result, beside num_turns), which is what --max-turns counts. A result stored before this has no request count, and 0.3.0 recorded its num_turns as the session's turns: rebuilding such a run's report (or resuming it) counts the session's requests from its transcript and replaces that record, once (budget.session-turns-recounted), so the demo's report now says 30, not 61. Only a session whose transcript is gone keeps its num_turns, listed apart as num_turns:<session> (num_turns_sessions in the report's budget and the status JSON), never compared with the cap and never the run's largest count. A session the cap ended, or one whose model requests went past the cap, now adds one budget.exhausted event for that session, and the report says "N model requests at most in one session; cap 30 per session, enforced by the Claude CLI (--max-turns)".
No Inquisition starts after review_rounds reaches its hard cap. In the 0.3.0 retest's required run a last Inquisition ($0.04) ran after review round 4 of 4, and the run then ended EXHAUSTED anyway, since whatever an inquiry decides about a review needs another review. A review that does not clear the candidate with no round left now ends the run EXHAUSTED at that point, naming the open findings or the claims still needing a discriminating test, as a repair without a round already did; an inquiry entered to return to REVIEWING with no round left ends the run the same way without starting an inquisitor. Two model sessions can still start. One is an inquiry into review claims on a tree an approving review is on: refuting the claims clears the tree without another round, so that inquiry still runs, and when it does not, the run ends EXHAUSTED after it. The other is the curator, which records what the run learned after the run has ended, whatever the outcome (in the 0.3.0 Python run it started after EXHAUSTED): it changes nothing in the candidate, runs only when the closing reserve can pay for it and is capped at knowledge.curator_budget_usd ($0.25 by default).
One spend explanation in the report, orbit status and the timeline footer. The 0.3.0 demo showed cost_usd 3.65, "model cost: $1.9985 (incomplete: some usage has no cost)" and "measured $2.00 ... estimated $1.66; charged $3.65" without saying which figure the cap is checked against. Each now gives one line, for example "$3.65 charged to the cost budget (the figure the cost cap is checked against): $2.00 reported by providers (5 sessions) + $1.66 estimated from tokens for 3 sessions that reported no cost", naming as well any ceiling charged for a session that could not be priced, the part spent before PLANNING, a charge with no matching usage record (without guessing its cause) and recorded spend the budget did not charge. The session whose charge crosses the reserve or the cap is now recorded with its usage before the run ends EXHAUSTED, so the spend line counts it among the reported sessions instead of as a charge with no record. The report's budget.spend and the status JSON's spend carry the figures; the report's "model cost" line is replaced by the spend line, and budget.cost_usd keeps its meaning. Each session counts as the charge its usage record's mark names, so it is counted once, and a lost attempt and its restart, two sessions of one worker, are counted apart (a ceiling charge naming the worker stood for both, so a $6 lost attempt and its $0.50 restart read as $0 reported across 0 sessions and $0.50 charged with no matching usage record). Every session no provider measured, charged at a ceiling or with no reported cost, is named, in the line and in unmeasured_sessions. A run started before 0.3.1 recorded no marks: its line gives the figures as recorded, what its usage records say and the stand-in charges its budget recorded (legacy_charges), matched to nothing, and says the run is accounted under the rules of the version that started it (accounting: legacy).