Single source of truth for project rules, architecture, commands, and conventions. Agent-specific entry
points (e.g. CLAUDE.md) import this file rather than duplicating it — edit here once.
Governor — a deterministic shell around a model core. It is the feedback controller that makes an autonomous coding-agent loop safe to leave unattended: speed limiter, circuit breaker, and done-as-proof. It is not a loop runner, daemon, or scheduler — those provide the power; Governor governs.
Status: M0 scaffolding. Schemas + the deterministic decision surfaces are pinned and tested; the live
L2 judge, true-bill billing (GATE-B), and the govern run happy path beyond the dogfood contract are
intentionally deferred behind the M0 gate in README.md. Do not "productize" without checking that gate.
The repo dogfoods itself: loop-contract.yaml at the root governs Governor's own development loop. Its
L1 verifier is pnpm typecheck — the most un-gameable signal in this repo. The maker is forbidden from
touching dist/, .github/workflows/, and CONVENTIONS.md.
pnpm build # tsup all packages → dist/ (ESM). Run before tests; tests import workspace pkgs via dist.
pnpm typecheck # tsc --noEmit across all packages. This is the dogfood L1 gate.
pnpm test # pnpm build && node --test --import tsx scripts/run-tests.ts
# single test (build first — workspace package imports resolve to dist/):
node --test --import tsx scripts/run-tests.ts --test-name-pattern="FSM"
# the CLI (binary: govern)
node packages/govern/dist/govern.js audit loop-contract.yaml # exit 2 = errors (CI gate), 0 = clean/warnings
node packages/govern/dist/govern.js init # scaffolds loop-contract.yaml + STATE.toml + .govern/
node packages/govern/dist/govern.js run loop-contract.yaml # needs `claude` (or `--maker codex`) on PATH
node packages/govern/dist/govern.js ack loop-contract.yaml reject|approve # human-ack a paused (AwaitingHuman) run- Package manager is pnpm@10 (enforced via
packageManager). Workspace layout:packages/*. - Node >=20, ESM (
"type": "module"), TSstrict+noUncheckedIndexedAccess+exactOptionalPropertyTypes. That means: array indexing yieldsT | undefined(guard it), and never assignundefinedto an optional field — build the object conditionally (see the adapters'if (usage) result.usage = usagepattern). - Each package builds with
tsupto ESM; the CLI bundles viatsup src/govern.ts. - Tests are a single
node:testentry atscripts/run-tests.ts(no jest/vitest). One tsx loader. It covers every deterministic surface the model is forbidden from touching, plus an end-to-endrunRuntest against a temp git repo with a stub adapter.
Read this before changing any of contract, state, guards, verifier, or govern. These invariants are
the entire point of the project; do not weaken them to "make it work."
Trust gradient: L1 (deterministic exit code) > L2 (heterogeneous LLM judge) > maker. The maker's self-report can never stop the loop. This is enforced at three independent layers — keep all three:
- FSM (
@governor/state): there is noMakerClaimsDoneevent.assertFsmInvariants()encodes this as a runnable assertion.CanStoponly exists asVerifyL2 → Done. - Type brand (
@governor/verifier):VerifierEvidenceis a branded type minted only inside the package bydecide().MakerClaimis structurally incompatible withcanStop(ev)'s parameter — it won't typecheck. Keep the brand private; the evidence minter must stay inside@governor/verifier. - Aggregator (
decide()): the only place evidence is minted, and the maker is not even an input to it.
The FSM is the spine. @governor/state defines Status + Event and an exhaustive LEGAL transition table;
anything not listed is illegal and rejected without persisting. Terminals (Done/Failed) have no exits.
Halted can never auto-resume — resumption requires a human ack from AwaitingHuman. When adding a state or
event, update the table and assertFsmInvariants().
STATE.toml is the only memory, written only by the deterministic shell. The model may READ it, never WRITE.
All writes funnel through the FSM and use atomic write (temp file → fsync → POSIX rename). There are two
implementations of the atomic-write store (store.ts FsStateStore and run.ts's own copy) — govern run
uses its own tolerant read/normalize path because TOML drops null keys on stringify; both must stay atomic.
Proof-before-Done: the stop_proof artifact is atomically written before the CanStop transition to Done.
If the proof write fails (disk full / EACCES), STATE stays non-terminal. Never reorder this.
contract_ref invariant: the loop-contract is pinned to a git sha at startup; a mismatch at runtime halts
(contract_ref_mismatch). This stops a maker from quietly rewriting its own rules mid-run. govern run seeds it
from git rev-parse HEAD; govern init uses a placeholder 40-zero sha that govern audit flags as an error.
Refuse, never silently degrade (P5). @governor/adapter's satisfies(actual, required) short-circuits to
false on the first unmet capability, and govern run refuses before touching a worktree. Silent degradation is
treated as the deepest trap (strongest runtime converges on weakest). Do not "fall back" when a capability is missing.
progressHash is governor-computed, never maker-supplied. It folds in diff_hash + normalized todo/completed/
deduped errors. Including the diff hash is load-bearing: a maker that repeats its todo but produces an empty diff
(the real "spinning" signal) must still churn the hash. Inputs are sorted/deduped so reordering alone doesn't count
as progress.
Verdict inconclusive with env_broken halts and is never retried (the Noor case: a broken compiler / missing
binary / hung env must not loop). transient inconclusive blocks stopping but does not halt.
Package dependency direction (one-way): adapter, state, guards, verifier all depend on contract
(never the reverse); the shared capability + halt-reason vocabularies therefore live in contract. govern (the
CLI) depends on everything. Adding a reverse dependency is a design violation.
Command-injection posture: shell commands from the contract (trusted, frozen) run via sh -c. Runtime
inputs (worktree, the prompt) go through cwd / stdin only and are never spliced into a command string.
Adapters spawn with shell: false + argv arrays. Preserve this when adding adapters or commands.
guards(state, contract) # iteration → no-progress → budget; first trip wins, in fixed order
→ maker: adapter.headless() # produces an untrusted diff
→ L1: L1CmdVerifier.evaluate() # exit code is the only un-gameable signal
→ L2: placeholder (M0) or null in L1_ONLY mode
→ decide(l1, l2, mode) # deterministic aggregator + sole evidence minter
→ canStop(evidence) ? write proof → CanStop(Done) : applyTransition(IterDone) and loop
done_when: 'L1.all_pass' selects L1_ONLY mode (dogfood / strongest-signal-only); the default
'L1.all_pass AND L2.all_pass' requires both. L1_ONLY treats L2 as exempt (evidence.l2_pass = true), not absent.
- Comments are dense and load-bearing — many encode a why (a defended trust boundary or a past failure mode). When editing, match the existing comment density; do not strip explanatory comments to "clean up."
- Fail-closed by construction: the contract schema uses
z.literal/z.enumso any unknown stop predicate or illegal field fails validation rather than being ignored. - The
.govern/dir holds per-run state, worktrees, andstop-proof-*.jsonartifacts — gitignored runtime data. docs/holds the architecture deep-dive (governor-final-review.md), the decision pack (governor-decisions.md), and the Loop-Engineering research this project grew out of. Consult these before non-trivial design changes.
Commits follow the Conventional Commits specification. Each commit message uses this format:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
- type (required, lowercase) — one of:
feat— a new capability or surface in a packagefix— a bug fix or correctness fix (incl. an FSM / guard / verifier invariant regression)docs— documentation-only changes (README.md,AGENTS.md,docs/*)refactor— code/structure change that neither fixes a bug nor adds a feature; invariants must stay intacttest— adding/correcting tests/fixtures inscripts/run-tests.tsbuild— build tooling or dependencies (tsup,tsconfig,package.json)ci— CI/automation (note:.github/workflows/is forbidden to the dogfood maker)chore— routine maintenance (.gitignore, dependency bumps)
- scope (optional) — names the affected package/area, e.g.
feat(verifier),fix(guards),feat(state). Use the package's short name:contract,state,guards,verifier,adapter,adapter-claude,adapter-codex,testkit,govern. Repo-wide changes usechore(repo)ordocs. - description (required) — imperative mood, lowercase, no trailing period, focused on the what/intent.
e.g.
feat(verifier): mint evidence only inside decide(),fix(guards): short-circuit on first unmet capability. - body (optional) — wrap at ~72 columns; explain the why, not the what. Omit if the description is self-explanatory. For changes touching a load-bearing invariant, name the invariant here.
- footer (optional) — reference issues/tickets (
Closes #12) or add metadata. - Breaking changes — append
!after type/scope (feat!:/feat(state)!:) and/or add a footer line starting withBREAKING CHANGE:describing the change and migration. In this repo, anything that weakens a load-bearing invariant (trust gradient, FSM table, atomic write, proof-before-Done, contract_ref, P5) is a breaking change — mark it as such so it is reviewed as one.
feat(verifier): mint VerifierEvidence only inside decide()
fix(guards): short-circuit satisfies() on first unmet capability
Previously a partially-satisfied capability set could let a degraded runtime
pass the gate. First unmet key now returns false immediately (P5 — refuse,
never silently degrade).
Closes #8
feat(state)!: drop MakerClaimsDone from the legal transition table
BREAKING CHANGE: the maker's self-report can no longer move state at all;
any caller that relied on it must drive stops through decide()/canStop().
Describe the change and which invariant (if any) it touches. List the validation commands run — at minimum
pnpm typecheck (the dogfood L1 gate) and pnpm test; call out any path you intentionally did not run.
The PR title must itself be a valid Conventional Commit subject. Link related issues when available.