Invariants: the safety properties Orgabot enforces
The load-bearing safety and correctness properties. Each was written after something broke silently, and no change may weaken any of them.
Last updated
These are the properties a change must never weaken. Most exist because something failed silently: HTTP 200, nothing in the logs, nothing visibly broken, and the wrong outcome shipped.
| Invariant | What it protects against | Where it applies |
|---|---|---|
| Reviewer is never the producer | An agent approving its own change | The independent review |
| Evidence over confidence | A gate passing, or a diff reading as "no changes", without proof | Verification gates |
| No authority from natural language | A worker acting on words instead of granted tools | Brokered tools and the mission brief |
| Draft PRs are intentional | A pull request reading as ready before the merge path decides | Opening and merging PRs |
| Pending records are reaped from observed facts | Undelivered work deleted on a cached or unreadable belief | pending.json and its reapers |
| Salvage-commit hygiene | Build artifacts and huge files committed from a dead worker | The salvage commit path |
| Session-limit death is a failure | A usage-limited worker reported as "completed / no changes produced" | Worker failure mapping |
| Durability is proven, not asserted | Journaled steps re-executing, reopening a worker or re-charging cost | Restate resume tests |
| Mission state needs one authority | Competing sources of truth for one mission | Lifecycle work and its projections |
| Interrupts are acknowledged transitions | The UI reporting an interrupt the runtime never performed | Escape and other UI interrupts |
| Autonomy uses the org chart | An agent self-granting authority or escalating without routing | Roles and capabilities |
| An org-owned mission is owned by a role | A silent fallback to the generic worker | Role selection at launch |
| Local macOS MVP first | Planned providers or remote work landing on the critical path | The delivered boundary |
| The PR body is the only closing path | A title keyword closing an issue on merge | PR titles, merge messages, the body footer |
| A workflow may not drop a gate | A workflow or configuration skipping a policy-required gate | Workflow resolution, fail-closed |
| The operational store holds no authority | A second authority, or a fallthrough to leftover JSON | The SQLite operational store |
| Connectors that observe never act | A mutated backend, or a failed poll read as an all-clear | The Firebase connector |
| Recovery acts only on live, first-hand evidence | Resuming a PR on a stale record or weaker gates | The recovery queue |
| Dashboard actions repaint before the network | A dashboard that feels laggy | State-changing dashboard handlers |
| GitHub is polled at a bounded rate | Exhausting the GitHub API quota | GitHub polling |
Reviewer is never the producer
The independent reviewer must never be the same agent that produced the change.
Evidence over confidence
A verification gate passes only with command exit-code evidence. A diff touching at least one file (including a binary add, delete, or rename) must never be treated as "no changes produced": the no-changes heuristic counts changed files, not inserted or deleted lines.
A related trap: a shell operator inside a verify command can make the gate measure nothing at all. cd x && npm test once ran /usr/bin/cd, exited 0, and the gate reported PASS having tested nothing. Two PRs merged on fabricated evidence before it was caught.
No authority from natural language
Workers act only through brokered, policy-checked tools. The mission brief labels every piece of context by trust level. Editing a branch is auto-approved; opening a PR, merging, or deploying require policy or explicit approval.
Draft PRs are intentional
Pull requests open as drafts. Auto-merge paths promote a draft to ready in the shared merge path before merging. Do not stop opening PRs as drafts.
Pending records are reaped from observed facts
pending.json is built-but-undelivered work. A record is deleted only when its PR is observed merged (by any route, not just Orgabot's own ship path), or when its branch exists neither locally nor on the remote. "Actionable" is always derived from those facts, never from the record's own say-so.
- Deletion needs a first-hand, repo-scoped fact. PR status is keyed by repo root, never by bare branch name across repos.
- The dashboard pass reaps only
pr_mergedandinvalid_branch.branch_goneis left toorgabot pending reap, which reads git live, because the dashboard's branch lists are a TTL cache and a cached negative is not an observation. - A record may never name the base or default branch. That is refused at the write, and again at the storage boundary, because
orgabot shipwould then targetmain. - A record's identity is the repo-scoped
(project, branch)pair. Branch names are unique per repository, never globally, so the scoping lives in the lookup rather than in a caller's filter. - Writes are atomic and lock-guarded, so the dashboard reaper cannot clobber a concurrent write: temp plus rename while
pending.jsonis authoritative, one SQLite transaction once the domain has switched. - Uncertainty keeps a record. An unreadable source is not evidence of absence, and a branch list read before a record was written is not evidence about that record. A write refuses rather than rewriting the store from a list it could not read, and an empty query result is an answer about what was asked for, never "the record is gone".
Salvage-commit hygiene
On worker death or timeout, the salvage path commits work in progress but must exclude untracked build-artifact directories (.build/, node_modules/, dist/, target/, DerivedData/) and untracked files over 5 MB. Already-tracked files are still committed. Normal, non-salvage commit paths are unchanged.
Session-limit death is a failure, not a success
A Claude Code session or usage-limit message in the stream maps to a distinct worker failure. It must never become a silent "completed / no changes produced".
Durability is proven, not asserted
Restate resume tests assert that journaled steps do not re-execute (the worker is opened once and its cost is not re-charged), and that a mid-flight worker resume restarts at the exact next event index.
Mission state needs one authority
New lifecycle work must make legacy mission records, organization mission records, objective nodes, inbox items, and dashboard cards projections or compatibility references, never competing sources of truth.
Interrupts are acknowledged transitions
Escape or another UI action requests interruption. The UI must not report a mission interrupted until the runtime acknowledges it. When escalation is required, the worktree, session, branch, and recovery evidence are preserved.
Autonomy uses the org chart
Agents consult and route through roles and capabilities before escalating. No agent may self-grant authority. Humans enter only where policy, risk, spending, reserved authority, or configuration requires them.
An org-owned mission is owned by a role
The generic unattributed worker is sanctioned only for a project no organization maps, or when the operator passes --no-org. When an org maps the project and no role can be auto-selected, the mission holds and prompts for a role. It must never silently fall back to the generic worker.
Local macOS MVP first
Local macOS remains the zero-cloud default and critical path. Later accepted decisions add cross-platform secret custody, hybrid providers, isolated hosted tenant control planes, and the published shared GitHub App without weakening that profile. Providers still marked planned in the generated deployment matrix, plus mobile and cross-device interaction, remain outside the delivered boundary.
The PR body is the only closing path
No Orgabot-composed pull-request title or merge-commit message may carry a GitHub closing keyword. A keyword in a title links nothing while the PR is open, but GitHub reuses the title as the default-branch commit message on merge, and there it closes the referenced issue and bypasses the deliberate Closes / Refs decision made in the body. Titles say Fix issue 1234, never Fix #1234; the body's single footer is the one intentional closing keyword.
That decision itself is never a bare "the diff passed its gates": Closes #N requires the issue's own acceptance criteria to be MET by what the mission delivered, an independently confirmed tip (verified and reviewed), and no structural block (an epic/design/P0 label or title, an open child, or a PR summary that reads as a partial delivery). Anything less writes Refs #N and leaves a comment on the issue naming what was and was not delivered. See Shipping and review for the full rule. One acceptance evaluation decides both this footer and whether the mission itself may terminate completed, so the two can never disagree.
A workflow may not drop a gate
Missions execute a workflow definition resolved organization, then project, then mission, and policy is checked at resolution, fail-closed: a workflow omitting a policy-required gate is refused before a run exists, an unreadable policy blocks rather than reading as "no policy", and a policy-locked gate cannot be disabled by configuration at any level. Evidence-over-confidence, reviewer-is-never-the-producer, and draft-PRs-are-intentional are gate properties now, so weakening one weakens every gate. A refused gate decision is recorded with its reason, never dropped: a pass the rules did not admit must not read like one they did.
The operational store holds no authority
The local SQLite operational store serves listings and change notifications, and it is the only store for the peripheral domains (approvals, inputs, pending records, organization state, the observation stores, acknowledgments) and for mission records. The lifecycle journal stays mission authority and the event spine stays event authority. Those domains were moved into the store behind proven equivalence (ADR 0063) and the dual-write phase is complete: there is no JSON copy, leftover JSON is unread, and no path falls through to it, including error paths. An unavailable store refuses.
Connectors that observe never act
The Firebase connector polls App Hosting rollouts read-only so a failed rollout becomes a dashboard incident; it never deploys, rolls back, or mutates a backend. A failed or partial poll keeps the previous observation and reports the error, because replacing the list with an empty one is a false all-clear, the exact failure the connector exists to prevent.
Recovery acts only on live, first-hand evidence
The recovery queue may resume a stranded Orgabot PR, but every decision re-reads the PR's live state first: a record is a cached belief, and an unreadable state holds. A resumption re-enters the ordinary mission path under the original mission's org and role, so every verify, review, and merge gate still applies. A fork's head branch is not proof of Orgabot's authorship, a cancelled owner is an operator decision no sweep overrides, and nothing reachable from an unauthenticated read endpoint may spawn a worker or spend a credential.
Dashboard actions repaint before the network
A state-changing handler applies its change locally, repaints, sends the request, and rolls back on refusal. await fetch(...) followed by a poll with nothing painted in between is the shape that made the dashboard feel laggy.
GitHub is polled at a bounded rate
Never once per UI poll. A per-batch cap is not a rate limit: a caller that polls every few seconds starts a fresh batch as soon as the previous one finishes, so a large backlog drains at poll speed. That once exhausted a 5,000/hour GraphQL quota in about 14 minutes with no missions running.