Skip to content

Latest commit

 

History

History
185 lines (142 loc) · 11.2 KB

File metadata and controls

185 lines (142 loc) · 11.2 KB

Repository Guidelines

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.

What this is

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.

Build, Test, and Development Commands

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"), TS strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes. That means: array indexing yields T | undefined (guard it), and never assign undefined to an optional field — build the object conditionally (see the adapters' if (usage) result.usage = usage pattern).
  • Each package builds with tsup to ESM; the CLI bundles via tsup src/govern.ts.
  • Tests are a single node:test entry at scripts/run-tests.ts (no jest/vitest). One tsx loader. It covers every deterministic surface the model is forbidden from touching, plus an end-to-end runRun test against a temp git repo with a stub adapter.

Architecture — the load-bearing invariants

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:

  1. FSM (@governor/state): there is no MakerClaimsDone event. assertFsmInvariants() encodes this as a runnable assertion. CanStop only exists as VerifyL2 → Done.
  2. Type brand (@governor/verifier): VerifierEvidence is a branded type minted only inside the package by decide(). MakerClaim is structurally incompatible with canStop(ev)'s parameter — it won't typecheck. Keep the brand private; the evidence minter must stay inside @governor/verifier.
  3. 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.

The loop (govern run)

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.

Coding Style & Conventions

  • 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.enum so any unknown stop predicate or illegal field fails validation rather than being ignored.
  • The .govern/ dir holds per-run state, worktrees, and stop-proof-*.json artifacts — 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.

Commit & Pull Request Guidelines

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 package
    • fix — 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 intact
    • test — adding/correcting tests/fixtures in scripts/run-tests.ts
    • build — 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 use chore(repo) or docs.
  • 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 with BREAKING 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.

Examples

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

Pull requests

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.