- Fix — when X, should Y, actually Z. Example:
#1074 — when
gh auth statusexits 1 while the active account is valid, the Runner should resolve it; actually the exit code failed the delivery. - Change — when X, the user should be able to Y; today they
cannot because Z. Example: #1048 —
when a user starts the runner from the published image, they should
get a working model provider; nothing read the
ORBI_PI_*variables yet.
ai-blocked scene that needs a human). This page covers the complete
automatic chain and the project’s GitHub workflow vocabulary.
The complete chain
State machine (the seven delivery states; the review/fix loop stays on the same PR — only its head may advance):ai-fix-needed with
the failure comment on the Issue AND the PR carrying the full scene — and
the next tick resumes the same run, branch, worktree and PR (no
replacement PR, no re-claim). A missing worktree is not a failure at
all: the resume rebuilds it from the remote delivery branch and
continues (Issue #807 — the worktree is a local cache of the remote
state, branch + PR live on GitHub). Only an external precondition the AI cannot
safely judge or fix (an unrecoverable scene, a base-branch config change,
an exhausted review round budget, a PR closed without merge, a delivery
branch deleted from the remote — nothing left to rebuild the worktree
from) fails fast:
the Issue is marked ai-blocked with the explicit reason why automatic
recovery is impossible, and the automatic loop never touches it again — a
human decides the next step.
Steering an active delivery
You can correct direction without cancelling an active delivery. While the Issue isai-in-progress, add a comment with the new decision or edit the
Issue body. These two channels are equivalent: both are picked up by the
Runner’s next steering poll (the interval is steering_poll_seconds, 60
seconds by default), and the active session restarts with the correction. The
restarts are bounded by steering_max_rounds (3 by default); after the limit
is reached, further corrections do not restart that delivery and the journal
records steering_limit_reached. Let the current delivery finish, then put the
revised scope in a follow-up delivery.
Only comments from trusted repository roles—OWNER, MAINTAINER, MEMBER, or
COLLABORATOR—can steer a run. A comment from a public commenter is ignored by
design, because allowing arbitrary commenters to steer a delivery would expose
a prompt-injection path. The initial trusted comment timeline is capped at
issue_comments_limit (200 by default): older comments are dropped and the
injected context states how many were omitted.
Dead-loop guard and comment dedup (Issue #825): a resume may bind a NEW
run id (one recoverable failure is enough), while the PR body is written
once at creation, so the resume verification accepts ANY run marker the
delivery line has used — the marker set is read from the Issue’s TRUSTED
comments (the same trust filter as the resume scene; a copied marker in a
public comment widens nothing, and the branch, base, head and exact-URL
checks stay intact, keeping the #45/#89 anti-PR-swap posture). A failure
comment of one (run_id, failure fingerprint) pair is posted ONCE (the
hidden <!-- orbi:fail=... --> marker is the key); every identical
repeat bumps that comment’s hidden :<count> suffix in place (the
journal records the repeat). The same pair recurring to 3 CONSECUTIVE
failures (a scene block — a newly opened PR, a completed review round —
means the line advanced and resets the streak) is classified as a dead
loop, not a transient error: it escalates to ai-blocked with the count
and the unchanged-precondition verdict in the comment.
Rules of the chain:
- One task = one run = one feature branch = one worktree = one PR. The PR number never changes during the review/fix loop; only its head may advance.
- Only the two opened-PR states are picked up automatically:
ai-pr-opened(awaiting review) andai-fix-needed(awaiting the next review session).ai-readyis claimed as new work;ai-blockedis never auto-recovered. A delivery that carries the backfilledai-in-progresslabel is still picked up (Issue #178: a runner killed during review leaves it behind — the positive opened-PR qualifier already keeps implement-phase Issues out of the scan). - The review/fix loop is bounded (5 rounds). The completed-round count
lives in the trusted scene (
review_round, Issue #788): every round comment carries the updated scene block, so the next tick reads the advanced count from it (a human’s blocked -> ai-fix-needed recovery after the scene still starts a fresh budget, Issue #483). Exhausting rounds with findings marks the Issueai-blocked(the bounded loop is a human decision); a recoverable failure (Issue #50) keeps itai-fix-needed. Either way the PR, branch and worktree stay intact. The reviewer guards the budget itself (Issue #878): before the work its prompt reads the delivery’s round comments (the samerun_id) and, when the same failure reason already appears in two or more consecutive rounds, emits the findings verdict naming the repetition instead of repeating the fix — a wall is hit by at most two rounds. - No in-tick waiting (Issue #788): the old in-process wait loop
(
wait_for_delivery, CI/mergeability polling withrelease_ci_wait_seconds/mergeable_wait_seconds) is gone. Every wait is the waiting primitive — write the state, return, release the slot, and the next tick re-reads (delivery_ci_pending,merge_gate_ci_pending,merge_gate_mergeable_unknown). The resumable scan skips a CI-pending candidate outright (Issue #1473), so a pending PR never costs a whole tick. The crash-recovery path IS the path. - Base advances are absorbed by a plain
git mergeof the latestorigin/<base>on the task branch (conflicts resolved manually), followed by a full test rerun. No force push, no auto conflict resolution, no push of the protected branch. - Absorb contract (Issue #877, machine-checked by the Runner): a review
round that starts with the head behind
origin/<base>or facing a merge conflict must end in one of two states — the branch containsorigin/<base>(git merge-base --is-ancestortrue), or the verdict isfindingsstating that the round attempted the absorb and abandoned it, with the concrete reason. Apasswhose head still lacks the base is named in the round comment as an absorb-contract violation (review_absorb_abandoned); the bounded 5-round semantics are unchanged. - Git transport (Issue #114, #580): git data operations go over the
configured transport (
git_transport:sshby default — git over SSH —, orhttpswith theghcredential helper), GitHub API operations stay on theghtoken — the full two-channel boundary and the pre-start check: Operations; the credentials: Security.
Reviewing with a different model
The independent review does not have to run on the model that wrote the code:review_pi_provider, review_pi_model and review_pi_thinking
select a separate provider/model/thinking level for the review session,
so the reviewer does not inherit the author’s blind spots. Each key
falls back independently to its implementation-side value
(review_pi_provider → pi_provider, review_pi_model → pi_model,
review_pi_thinking → pi_thinking), so overriding only one of them is
valid. Both selections must exist in the configured pi_providers
catalog and are validated separately at startup — an invalid review
selection fails fast with review provider selection invalid. No
specific pairing is recommended or benchmarked here; the capability and
the fallback are the contract.
The merge record: external_commits and commits
When the Runner merges a delivery PR, the Orbi merged PR: Issue
comment shows the user only what they act on — the PR link, the review
result and the target branch — and folds the merged-as-is facts
(Issue #833) into one collapsed Run details block:
Run details rows keep the key=value form (equals sign, not
colon) because they are machine-read anchors: the orbi-cloud
mergeCommentFields reader and the repository’s comment-reader
contracts match external_commits=K, review_rounds=N and run_id=…
exactly. The Orbi: merged milestone line carries the same facts in the
same key=value form.
commits— all commits of the PR branch relative to the base at merge time (the denominator of the ratio).external_commits— commits NOT in the engine’s own push history (the head the PR opened with plus every review/fix head the Runner itself pushed; git authorship is never inspected).0= merged as-is: no human changed a line on the delivery branch before the merge. The count is read after the merge through the merge commit’s parent chain (M^1the base tip,M^2the PR head) minus the engine’s recorded push line: the last engine-pushed head, and — for an external takeover (Issue #608) — excluding the taken-over contributor head the line starts from, so the contributor’s commits stay external.external_commits=unknown— the push history is unverifiable (a recreated worktree lost the run-state record, a fix push’s round ended before its record and the history is incomplete, or a recorded head or base is not an ancestor of the merged head). A squash or rebase merge (Issue #1480) has noM^2, so both metrics areunknownthere as well. An uncountable history is never written as0.
pushed_head, Issue #219 file) at three points — the delivery push,
a verdict-covered review/fix push, and the round-start adoption of a
head a previous session pushed but whose round ended before the
re-freeze record — so resumes of the same worktree keep the history; a
fresh worktree starts without it and degrades to unknown. An
external takeover additionally records the taken-over head once at
claim time (pushed_base), and its round-start adoption never runs:
the worktree legitimately starts on the contributor’s head. Recording
is bypass-safe: a missing or corrupt state file logs
pushed_head_unrecorded and the delivery continues.
Delivery labels
GitHub labels are the external state machine. They are not created by commits — initialize them per repository (see Getting started):
A repository policy may block the merge even after Orbi’s review and checks
pass. Orbi stops at
ai-awaiting-merge; the Issue comment names the exact head
and the one maintainer action (including the settings URL when applicable).
After the maintainer changes the policy or supplies the required approval, a
later tick resumes and Orbi merges the PR itself. A policy rejection without a
known FAILED preflight action, or any other merge failure, remains
ai-blocked.
Human acceptance gate (ai-human-review)
The main line of the product is unattended: the default pipeline runs with humans OUT of the loop, and the gate is off by default — with the switch off the delivery, review, merge and release behavior is exactly what it was before (Issue #763). The host config turns the gate on (human_review_gate = true in
orbi.toml — an operator trust decision, not a repository policy).
Every delivery then posts ONE acceptance checklist comment to the Issue
when its PR opens, split in two columns:
- Column 1 — what the run already verified (evidence, not todos): the PR head’s CI check status (Issue #1613: the engine never parses a local test log), the covered layer (the repository test suite), the delivered file count.
- Column 2 — the machine-unverifiable minimum, derived per delivery from the changed paths and the available evidence (never a hardcoded human-items template): business intent (tests prove the implementation matches the tests, not that the tests match the intent), UI visuals and copy, external-environment verification, and missing-evidence items when the head’s checks have not passed or the diff cannot be read.
ai-ready (the opened-PR label stays as the resume anchor), the slot
is released, and every later tick costs one label read. The wait does
NOT consume the review-round budget and never touches
ai-fix-needed. A person who confirms the checklist applies
ai-human-review, and the next tick reviews and merges the delivery
exactly as it does today (the verdict binding and
--match-head-commit are untouched).
An EMPTY column 2 is the target state: the checklist says
无需人工介入 (“no human intervention needed”) explicitly and the
delivery passes without the label. As model capability grows,
column-2 items migrate into column 1 — the checklist comment carries a
machine-parsable <!-- orbi:human-review {...} --> block (schema 1)
so the migration is toolable without changing the artifact shape. The
checklist makes no correctness guarantee of any kind: column 1 is
evidence of what was verified, column 2 is what a person still
confirms.
Thin-ticket clarification gate (ai-needs-detail)
A confident delivery against a vague ticket is worse than no delivery: it burns a run, opens a PR nobody can accept and buries the real question. The gate is OFF by default and a repository turns it on withclarify_thin_tickets = true in .github/orbi.toml; an unopted
repository behaves exactly as before.
With the gate on, a fresh claim first asks ONE Pi session — --no-tools,
no worktree, no branch, no delivery — to judge the ticket body on three
checks: the observable result (what a user sees or does differently),
the acceptance condition (how a finished delivery is told apart from
an unfinished one) and the single outcome (one fix or one change, not
several unrelated things). A passing verdict changes nothing and the
delivery starts.
A failing verdict stops the attempt before any work exists: ONE Issue
comment lists every missing piece and the repair action, the repository’s
claim label (ai-ready by default — dispatch_label may name another)
is replaced by ai-needs-detail, and the tick ends — no worktree, no branch,
no Pi session, no PR, and the slot is released for the next Issue. The
ticket then waits for a human exactly like ai-blocked: the next chance to
be delivered comes from editing the Issue and adding that claim label
again, which re-runs the judgment from scratch.
Every error path is a bypass — the gate never loses a delivery. A model
error or timeout, an unusable answer, or a failed comment/label write logs
the clarify_check_skipped journal line and lets the delivery proceed.
The judgment and the stop are visible as the clarify_check_skipped and
clarify_needs_detail rows in
Operations.
Ticket dispatch: content, dev and ops
ai-ready is the execution switch for every task type; a second label
selects the type (Issue #537). Three types exist — pick one per Issue at
dispatch time:
Ops semantics (Issue #537):
- The trust boundary is (credentials, ticket) — never a command
whitelist. The session receives the runner’s whole environment
unfiltered (gh auth,
CLOUDFLARE_API_TOKEN, AWS keys, … plus the deploy-home.orbi/envfile); how wide the credentials are is the user’s decision — the runner never trims them. - The ticket body is the authorization. The session executes only the ops actions the ticket states (which PR to merge, which environment to deploy); an action beyond the ticket is reported in a comment and the session stops. Human approval gates (environment approval, Release approval) are never performed by the session — a prompt convention, not enforced code.
- Delivery = evidence (the fact culture of Issue #526): every step
posts the real command and its real output / API response verbatim on
the ticket; a step not executed is labeled as not executed. A
pure-ops run (no commit) ends with the Runner closing the Issue and
removing
ai-in-progress— no PR. Committed code takes the normal PR path with review and merge.
ai-ready +
ai-ops-only. A dev ticket cannot carry that instruction: the dev
delivery criterion is a commit on the task branch, so an agent that
correctly follows it delivers no commit and the run fails with
the agent delivered no commit — the Issue is burned ai-blocked.
The ops playbook’s evidence discipline (real commands, verbatim
output, unverified items labeled as such) IS the research
deliverable; a no-commit ops run completes normally (the Issue is
closed, ai-in-progress removed), and research that continues into
code still takes the normal PR ceremony.
Before Issue #537 the label ai-ops-only dispatched the content agent
(the #530 rename never changed the behavior — the name promised ops,
the mode was content-only); the content path dispatches on
ai-content-only now, and ai-ops-only is the full-execution ops
path.
Run marker and run_id
Every task attempt generates onerun_id (8 hex chars, e.g.
e07383c2) and reuses it for every step of the attempt; a retry of the
same Issue generates a new one. The same id appears in:
- every journal line of the attempt (prefix
[e07383c2]); - the Issue/PR comments: visible field
run_id=e07383c2plus the hidden machine-readable marker<!-- orbi:run=e07383c2 -->; - milestone and scene comments also carry the hidden runner marker
<!-- runner=1a2b3c4d -->: a fingerprint of the deployment that wrote them — the executing checkout’s HEAD, the package version for a non-editable install, orunknownwhen the probe fails (the live progress comment and PR comments do not carry it); - the
Orbi opened PRscene comment carries the hidden versioned run scene<!-- orbi:scene:v1 {json} -->(src/orbi/scene.py): the machine-readable recovery record the resume scan parses — a corrupted scene and a missing scene are distinct failure branches (Issue #786); - the stable Issue-based feature branch (
orbi/<owner>-<repo>-issue-<N>); - the worktree name (
.worktrees/orbi-<owner>-<repo>-issue-<n>-e07383c2), plus the Pi session directory and run artifacts; - the PR body — the stable marker
<!-- orbi:run=e07383c2 -->is part of the PR contract; the Runner rejects a PR without it.
Failure record: orbi:failure:v1
Every failure the engine reports through its failure path is
machine-readable: both the detailed Orbi: blocked / Orbi: fix needed
comment and the short milestone posted beside it carry exactly one hidden
versioned block next to the run marker:
outcome is blocked (terminal, a human decides) or fix_needed (the
next tick resumes the same run). reason_code says why the delivery
stopped; action_code says what the reader should do; retry_safe says
whether a plain re-queue can succeed without a human repair. The sets
are closed and owned by the engine (src/orbi/failure.py); an exception
the classifier does not recognize maps to unclassified, the block is
never omitted, and a comment written before this feature carries no block
at all — readers ignore it (parse returns None).
Actions:
requeue relabels the Issue ai-ready (or presses retry) —
nothing to fix; fix_ticket fixes the failure and re-runs the Issue;
decide is a human decision; check_credentials verifies and repairs
the credential, then retries; wait_quota retries after the quota window
resets. The readable headline, **Reason:** and **Action:** lines stay
beside the block for a human reader, and the milestone’s visible summary
is the same bounded, host-path free reason — a full command stderr is
never cut to its first word.
PR body contract: Fixes #N
The PR description must contain Fixes #<issue-number> (it may be on the
first line). GitHub reads the body (not the PR title) and closes the Issue
natively when the PR merges into the default branch. The Runner verifies
the keyword during PR acceptance and fails fast when it is missing. This
validation failure is recorded on the affected Issue and does not terminate
the runner tick.
A staged initiative must not use one Issue and one PR for several independent
runtime outcomes. Use an Epic or parent Issue for coordination, split each
phase into a child Issue, and give every child its own delivery PR with
Fixes #<child-number>. This preserves native close behavior without adding a
second PR-body syntax. If an existing PR says Part of #N, the maintainer must
update it to the exact Fixes #N reference when that PR is the complete child
outcome, or split the remaining phases into child Issues before retrying.
Epics, Release tasks and P0 priority
The project organizes multi-task work with plain GitHub primitives — no DAG, no priority numbers, no separate queues:-
Epic: a coordination Issue that groups a set of related tasks (for
example the v0.1 release checklist). It is marked with the
ai-epiclabel. An Epic is not itself a directly executable task: the actual work is split into independentai-readyIssues, each with one runtime outcome, one PR, one review and one merge. The Runner NEVER claims an Epic (Issue #93): the ready claim scan skips it with the structuredepic_not_claimed issue=N repo=...journal line (no claim, no label change, no worktree, no run, no slot), and the restart-resume scan excludes it too. Epic completion is declared exclusively by GitHub’s nativesub-issuesrelationship: every child Issue must be closed (or its PR is merged) and there must be no open nativeblockedBydependency. References in the Epic body, includingChildren/Child Issuessections, are not declarations. An Epic with no native sub-issues, or malformed child or dependency state, fails closed. The Runner reconciles open Epics on every tick and leaves an idempotent audit comment. Any prose describing extra acceptance conditions is an Epic definition error: make that condition a final child Issue instead (for example, a browser acceptance ticket). The same tick then reconciles open Milestones: an exact-title published Release (draft excluded) plus zero open Issues closes the Milestone; all other cases remain open. The same tick finally sweeps open PRs (Issue #746): a stable-delivery PR (orbi/<repo>-issue-<N>head branch) whose source Issue a human closed is reported ONCE on the PR with an idempotent comment — every resumable scan readsstate=openIssues only, so without this sweep the PR would dangle forever. The merge/close decision stays with the human: the Runner never auto-merges and never auto-closes across a human’s close. -
Release task: an
ai-readyIssue additionally marked with theai-releaselabel (Issue #98). It is NOT a development task. The three ordinary scans skip it withrelease_not_claimed; only the release fallback scan usesallow_release=True. Before claiming it,_pick_from_scanapplies the Milestone completeness gate: while its Milestone has any other open Issue (open_issues > 1), the Runner recordsrelease_milestone_incomplete, leaves the releaseai-ready, and retries on a later tick. This is a recoverable wait, neverai-blocked. A Milestone query that cannot be evaluated also skips the claim, withrelease_milestone_check_failed. This claim layer runs beforeprocess_issueand before any release-state-machine wait, so a release queued behind open tickets has nowait_startedvalue and accumulates zerorelease_waited_seconds. Once claimed,process_issueroutes it to the deterministic release state machine instead ofrun_pi— no Pi session, no PR. The state machine is idempotent and resumable (a restart resumes the same run id/worktree) and runs these steps in order:- Strictly parse the
## Releasedeclaration from the Issue body:version,base_branch, andscope(a list of#Nitems) orscope_from_milestone, plus optionalversion_file(pyproject.tomlby default,package.json, ornone). The declaration carries NO test contract (Issue #569): test acceptance is the GitHub Actions CI result on the release commit (the Issue #268 CI-wait gate). Missing, duplicated or unknown fields fail fast. - Freeze the base — the release commit is exactly
origin/<base_branch>(fetched under the base-sync lock). Before any gate wait, the declared (or defaulted)version_fileis proven to exist at the root of that frozen tree (Issue #740): a mismatch fails fast at claim time, naming the supported ecosystem files that DO exist there and thenoneoption, never as a late FileNotFoundError after the gates and the scope verification. - Enforce the pre-release gates. Only after the claim layer above has
admitted the release can the gate-layer
release_waiting_deliverieswait for open Issues carryingai-in-progress,ai-pr-opened, orai-fix-needed(the release Issue itself is excluded). With a Milestone, only Issues in that Milestone hold the gate (Issue #671); without one, the repository-wide scan is unchanged. A bareai-readyIssue does not hold this gate. The bounded wait usesRELEASE_DELIVERIES_WAIT_SECONDS; exceeding it produces a delivery wait timeout and moves the release toai-blocked. Then require CI green on the release commit (no check runs passes with explicit evidence; a pending check is an intermediate state — the gate waits for the final conclusion, onerelease_waiting_cijournal line and one progress-comment update per poll, up torelease_ci_wait_seconds(default 1800 s); a wait timeout fails with its own reason, never reported as a CI failure). Open PRs are deliberately NOT a gate (Issue #608, maintainer ruling): an open PR is queue state, never a release premise — whether open PRs exist, how many, or who authored them says nothing about the release; a stranded PR is the delivery loop’s takeover/reconciliation job. A gate that cannot be checked because of a realghfailure is a failed gate. - Verify the scope item by item:
gh pr viewmust reportMERGED, otherwisegh issue viewmust reportCLOSEDwithstateReasonCOMPLETED— aNOT_PLANNEDclosure (duplicate / won’t fix) is annotated in the Scope evidence as excluded and never enters the Changelog, and a Changelog PR link is written only when its PR is contained in the tagged commit range (Issue #1492). The Changelog itself is derived from the tagged commit range (prev_tag..release_commit), never from the Milestone Issue list: the Milestone scope decides only whether a release may run, not what its notes say. For milestone-derived scope,open_milestone_evidencedoes not wait: still-open milestone Issues are excluded and recorded asNOT released (still open in milestone <name>): <item>. Checkbox parsing is forbidden — every scope item is checked against the live API. - Test acceptance is the CI-wait gate on the release commit: after
the version bump the gates re-run against that exact commit
(step 3’s
release_ci_wait_secondswait), and a red or timed-out CI takes the existing recoverable failure path (ai-blocked). No local test execution (Issue #569) — the release flow is bump → push → CI-wait → tag. If Actions is red or times out, the existing failure path blocks the release; the version bump may remain onmainas an acceptable state. The bump is idempotent, so repairing CI and retrying the release safely prepares the requested version again. - Tag: when the remote tag is absent, create an annotated tag at the
release commit and push it with a plain push (never
--force). When it exists it must point EXACTLY at the release commit — a mismatch fails fast. An existing tag is never moved, overwritten or force-pushed. - Publish the GitHub Release (idempotent) whose notes carry the full
verification evidence: version, tag, release commit, per-item
scope evidence, gate evidence, test evidence and the run marker.
The docs-site Release pages are NOT written by the state machine
(Issue #1483): the repository’s own
.github/workflows/release-docs.yml(Issue #1482) rendersdocs/release-<version>.mdxanddocs/zh/release-<version>.mdxfrom the published Release body, inserts the new version at the head of theReleases/发布navigation groups indocs/docs.json(both languages, latest first) and moves the(latest)title marker to it. Since Issue #1096 both groups nest one collapsedEarlier releasessubgroup — the automated sync puts the new release at the head and moves every entry beyond the three newest into that subgroup, so the visible group keeps the three newest releases. Idempotent: an existing page with identical content is neither regenerated nor overwritten, and a run with nothing to change commits nothing; an existing page with different content fails fast — the workflow run fails, never a silent skip. - Apply
ai-mergedand close the release Issue (terminal delivery transition); the merged event clearsai-readyandai-in-progress, so the released Issue endsai-mergedALONE (Issue #1526). - Close the Milestone whose title is EXACTLY the released version
(Issue #214), then write the success comment (release URL, tag,
commit, evidence, Milestone evidence). Exact title match only (no
guessing, no fuzzy match,
never a different Milestone): close it when it has 0 open Issues;
already-closed is an idempotent success (no reopen); a missing
Milestone or remaining open Issues fail fast with the version, the
Milestone number/url and the open issue list (never a silent skip).
The release’s own ticket is never counted as unfinished work
(Issue #808): it is being closed by this release, so a stale-open
listing of exactly that ticket — the issue index lags seconds
behind the close (Issue #754) — does not block or delay the close,
while any other open Issue still refuses it.
Any failure:
ai-blockedALONE (a release is a human decision point — no automatic retry), the failure comment carries the run marker and the concrete reason, and the exception propagates so the tick fails fast.
ai-releaselabel is a type marker, NOT a delivery state: it is not part of the delivery-state machine, not an exclusion of the ready scan, and the Runner never adds or removes it — only the human does (at dispatch time, together withai-ready). - Strictly parse the
-
P0 priority: the plain
p0GitHub label marks an urgent Issue (a production outage). It is NOT a delivery state — it only orders the ready pickup, never changes the Issue granularity, any delivery state, or the terminal-state semantics, and the Runner never adds or removes it. The ready pickup order is fixed:ai-ready+p0→ai-ready+bug→ plainai-ready(threegh issue listscans sharing the exact same exclusions and blockedBy semantics). P0 obeys every existing exclusion rule and the single-slot constraint: a blocked P0 is skipped (falling back to the bug/plain scans) and an in-flight P0 is resumed by the restart scan. A failed P0 run entersai-blockedALONE (the claim label is removed; theai-readyresidue is excluded by every ready scan), so no tick re-claims it — no infinite retry. There is no priority number or weighted queue.
blockedBy
relation (gh issue edit N --add-blocked-by M); the Runner reads the
field and skips Issues with open blockers. Depends on #N lines in an
Issue body are not parsed.
Repository-level delivery policy (.github/orbi.toml)
A repository can version its delivery policy as code. At claim the
Runner reads .github/orbi.toml from the repository’s default branch
tip through the GitHub contents API — never the task branch, never the
local worktree — and applies its whitelisted keys per key over the
host orbi.toml. A repository without that file behaves exactly as
before (zero change). The location is fixed; only the file NAME may be
overridden per source repo with the optional [[repositories]] entry
field config_path (default .github/orbi.toml).
Only delivery-policy keys are allowed:
active_milestone, it wins over the host config and that is the value
the claim scan uses. orbi milestone set <title> lands the new value in
the same place — the repository policy when it declares the key (a
contents PUT on the default branch), otherwise the host orbi.toml
line — and prints wrote: naming the target (Issue #1306).
A host-only identity/security key (see Security), invalid
TOML or a wrong type fails the claim fast with the offending key names
and the Issue becomes ai-blocked — one repository’s bad file never
affects another pool. Any other unrecognized key is ignored instead: one
repo_config_keys_ignored journal line names the key and the engine
version, the run comment carries repo_config_ignored, and the claim
proceeds with the known keys (Issue #1329). The file is read by whatever
engine version the delivery host runs, which is often older than the code
that wrote the file, so a new policy key must be safe for an older engine
to ignore: it defaults to the pre-feature behavior, and an engine that
does not know it delivers as before. The host-only set also owns host scheduling:
max_concurrency can never be delegated to a repository file — the
concurrency slot pool belongs to the deployment host.
dispatch_label must not be a delivery lifecycle label
(ai-in-progress, ai-merged, …): those stay host constants. The block
lands on the Issue the claim selected; a repository whose
dispatch_label is not ai-ready has no ai-ready candidate to block
when the file itself is malformed, so the fail-fast reason is the
journal’s repo_config_invalid line (no label change, no comment). The
run comment records repo_config: <sha>; when the sha differs from the
previous run it also carries the repo_config_changed annotation and,
when the previous content is readable, the effective policy diff as
repo_config_diff, so a change is always visible on the run. A read
that fails for a transport reason (rate limit, network) is logged as
repo_config_read_failed and the host config applies — the degradation
is visible as the missing repo_config field on the run comment.
Waiting for a Milestone decision: the one reply that answers it
When the whole chain goes quiet, the Runner says WHY instead of waiting silently. Two idle states are surfaced through the SAME decision notice — one Issue carrying the fingerprintorbi-milestone-advance old=<active> candidates=<titles> — and each of them is
answered by the same in-ticket command, a trusted /milestone <version>
comment posted on that notice:
Both notices are idempotent (one open notice per wait, never one per tick),
are closed automatically with a receipt once the wait ends, and never act by
themselves: the Runner never creates a release ticket and never advances the
Milestone without the reply. The reply runs the deterministic three-step
/milestone command (milestone → release ticket → active_milestone)
described in Operations; no model call is involved.