# `@geonosis/verify`

Through the front door: `geonosis verify` — the metapackage pins this and every other kit tool at
ONE version, and passes the exit code through unchanged.

One gate runner. Tiers of shell steps, run serially, recorded as a JSON gate report a hook can read.

A repo's gate is already a list of commands — it lives in a `verify` script, a CI job, and an
engineer's muscle memory, and the three drift. This runs the list from one place and writes down
what happened, so the Stop hook, CI and the ledger are all reading the same run rather than each
re-deciding what "green" meant.

## Install

```bash
pnpm add -D @geonosis/verify        # bun add -d, npm i -D
```

## `--prove` is the first thing to run after installing

```bash
geonosis verify --prove
```

It writes a temp config with one passing step and one that exits 3, runs the real binary over it in
a throwaway directory, and checks that the failure reached the report, that the right step is named
with the right exit code, and that the process exited 1:

```
PROVEN — a planted "exit 3" step reached the report and exited 1
```

A runner that reports green is worth nothing until it has been watched going red on purpose. It
touches nothing in the repo it is run from, so it is safe at any time.

## Config

`geonosis.json` in the directory you run it from:

```jsonc
{
  "verify": {
    "fast": ["pnpm typecheck", "pnpm lint", "pnpm test:unit", "pnpm ratchet --tier fast"],
    "full": ["fast", "pnpm build", "pnpm test:integration", "pnpm format:check"]
  }
}
```

A step whose text is another tier's NAME is that tier, inlined in place, however deep it goes. A
cycle is refused by the path it went round rather than discovered as a stack overflow.

### A step that runs per workspace, or per shard

```jsonc
{ "command": "bun run --cwd {workspace} test", "per": "workspace", "exclude": ["apps/web", "tools/*"] }
{ "command": "bun run --cwd apps/web test:e2e -- --shard={shard}/{shards}", "per": "shard", "shards": 2 }
```

`per: "workspace"` runs the command once per workspace, `{workspace}` filled in; `exclude` lists the
workspaces (paths or globs) it leaves out of that. An exclusion that matches no workspace is
refused: a stale one reads as a step that leaves something out and leaves nothing out.
`per: "shard"` runs the command `shards` times at once, `{shard}` and `{shards}` filled in; the
runner behind the command owns the slicing (Playwright's `--shard=1/2`), this only cuts the run
into pieces. A shard step runs whatever the change touched.

### A per-workspace step that runs as one batch, and says what each workspace did

```jsonc
{ "command": "pnpm --filter ./{workspace} test:unit", "per": "workspace",
  "batch": "vitest run --reporter=json --outputFile={report} {workspaces}",
  "also": ["plugin/", "tooling/"], "counts": "vitest-json" }
```

`batch` runs once with the affected workspaces in `{workspaces}`. `also` names paths that always ride
with it, so the whole unit class is one runner process and the step is not skipped when no workspace
was affected. `counts: "vitest-json"` says the batch writes vitest's JSON report to `{report}`: the
gate report then carries `workspaces` under the step, one `{ workspace, files, passed, failed,
skipped }` for every workspace of the batch (a workspace that ran no test is named with zeros). A run
that left no report it could read fails the step. Every step sees `GEONOSIS_SLOTS`, the heavy slots the
run was budgeted, for a command that passes the machine's number to the tool it runs; a task turbo
runs sees it only when `turbo.json` declares it (see the turbo step below).

`geonosis verify prove` runs each `also` path the plant reached alone and calls the step proven only
if each goes red; its result line ends with the size of the copy it made.

### A per-workspace step that turbo runs

```jsonc
{ "command": "pnpm --filter ./{workspace} typecheck", "per": "workspace", "turbo": "typecheck" },
{ "command": "pnpm --filter ./{workspace} lint", "per": "workspace", "turbo": "lint", "force": true }
```

`turbo` names the task. In a repo with a `turbo.json` and turbo in `node_modules/.bin`, the step is
one `turbo run <task> --continue --concurrency=<jobs> --filter=./<workspace> …` over the affected
workspaces, so a CI job that runs `turbo run <task>` and the local gate read one definition of green,
and turbo's cache answers the workspaces whose inputs did not change. `force: true` adds `--force`,
for a task whose `turbo.json` inputs do not cover everything it reads: its green is never replayed
from the cache. In a repo without turbo the step's own command runs per workspace, as it always did.
A workspace whose `package.json` has no script for the task is one turbo would run nothing in, so
the step's own command runs there instead, as it would without turbo: the verdict is the same with
turbo and without it. A step that counts a report runs no turbo task. `geonosis verify prove` runs
the task through turbo, forced and with `--env-mode=loose`, over the workspaces the plant reached:
turbo 2.11.8 runs a task in strict env mode by default and drops `PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN`,
which prove sets so that pnpm does not refuse to run in the copy, whose linked install it reads as a
changed workspace.

A task turbo runs sees the `GEONOSIS_*` variables of its tier (`GEONOSIS_BASE`, `GEONOSIS_SLOTS`,
`GEONOSIS_HEAVY_SLOT_HELD`, `GEONOSIS_GATE_RUN`) only when the repo's `turbo.json` declares them:
turbo's default strict env mode hands a task nothing else. A variable in `env` is part of the task's
hash; one in `passThroughEnv` is handed to the task and not hashed. Measured on turbo 2.11.8, a change
of a `passThroughEnv` variable replays the cache and a change of an `env` variable misses it. So:

```json
"tasks": {
  "test": { "env": ["GEONOSIS_BASE"], "passThroughEnv": ["GEONOSIS_*"] },
  "typecheck": { "passThroughEnv": ["GEONOSIS_*"] }
}
```

A task that selects by `GEONOSIS_BASE` puts it in `env`, so a green over one base is never replayed
over another; a task that does not read it, such as `tsc`, leaves it out of `env` and keeps its cache
when the mark moves. `globalEnv` and `globalPassThroughEnv` (`global.env` and `global.passThroughEnv`
under `futureFlags.globalConfiguration`) declare them for every task. `GEONOSIS_CHANGED_FILES` needs
no declaration: verify hands a turbo step no `--changed` flag, so no task reads it, and its value is a
new temporary file each run, which in `env` would miss the cache every time. `geonosis doctor --only
turbo-env` WARNs on a task that declares none of them.

## `proves`: how a step can be made to fail

A step may declare its own falsification beside its command:

```jsonc
{
  "verify": {
    "fast": [
      "pnpm typecheck",
      {
        "command": "pnpm lint",
        "proves": {
          "plant": "printf 'debugger\\n' > src/planted.ts",
          "expect": "no-debugger"
        }
      }
    ]
  }
}
```

```bash
geonosis verify prove fast
```

```
  PASS (unproven)  fast/1  pnpm typecheck — no proves declared — nobody has watched this step fail
  PROVEN           fast/2  pnpm lint — the plant made it exit 1 and its output carried "no-debugger"
prove fast: 1 proven, 1 unproven, 0 not proven
```

Each plant runs in a **copy of the tree** in a temp directory — one copy per step, so a plant left
behind never becomes the next step's finding, and the tree you ran it in is not touched. The step
must exit non-zero AND its output must contain `expect`; a red without that evidence is a red for
any reason at all, including the plant having broken the shell.

A step that reads git history declares `"history": true`. The copy is then a repository of its own
(the copy's files as one commit, `refs/geonosis/verified` at it), run with the host's `GIT_DIR`-style
variables and git config out of the way, so a plant is a change since the mark and the step can be
watched failing on it. `geonosis verify lint-config` is such a step.

Half a `proves` is refused at load: a `plant` with no `expect`, or an `expect` that is blank, would
pass on any failure. A `proves` on a step that names another tier is refused too — a plant belongs
to one command.

**A step with no `proves` is `PASS (unproven)`, and that fails nothing.** Unproven is not failure;
it is the absence of evidence, said out loud on every run — in `prove`, on the line an ordinary run
prints, in the report's `"unproven": true`, and in the Stop hook's green message. A run that prints
any `(unproven)` prints one legend line under the step list, naming each step's place in
`geonosis.json` (`verify.fast[2].proves`, `verify.fast[4].parallel[1].proves`) to declare
`proves: { plant, expect }` at. Exit 1 is for a plant that did not produce the red it promised.

**There is no default tier and there never will be one.** A default that happened to match one
repo's script names would run the wrong commands everywhere else and still report green. No
`geonosis.json`, no `verify` object, or an unknown tier is refused by name.

## Running it

```bash
geonosis verify fast
geonosis verify full --report .geonosis/full.json
geonosis verify full --exclusive --exclusive-timeout 900
```

```
  ok    fast/1  pnpm typecheck (2140 ms) (unproven)
  FAIL  fast/2  pnpm lint (1830 ms)
  skip  fast/3  pnpm test:unit (0 ms)
verify FAIL — fast — /repo/.geonosis/gate-report.json
```

| Exit | What it means |
| --- | --- |
| `0` | every step passed AND the report was written |
| `1` | a step failed |
| `2` | refused — no config, no `verify`, an unknown tier, an option we do not implement — **or the report could not be written** |

The last one is the one worth arguing about: a gate that cannot record has not passed. Exit 0 on a
green run whose report never landed would hand the caller a pass they cannot go back and read.

### How many steps run at once

The steps of one `parallel` group run up to `--jobs` at a time, in a tier and in `prove`. The
default is `GEONOSIS_JOBS`, else the machine's heavy budget, which the "heavy slots" section of
[`@geonosis/gate`](https://www.npmjs.com/package/@geonosis/gate) says how to set. A repository that
must never run more than two at once says so once, in `geonosis.json → caps.heavy`:
`{ "caps": { "heavy": 2 } }`. Every entry
point then reads it: a tier, `prove`, the Stop hook, and the `GEONOSIS_SLOTS` each step is handed.
The `budget` line of a run names what bound it.

### A red that is the network, not the code

A step that failed with a DNS failure (`getaddrinfo ENOTFOUND`, `EAI_AGAIN`), a reset, a timeout
or an unreachable network stays FAIL. Under it, the run says it is an environment failure, not a
code fault, and the report records the line as `environment`. `ECONNREFUSED` is not one: a local
server that never started is the code's or the setup's. `--retry-environment-failures <n>` re-runs
such a step up to `n` times, and never re-runs a red that is the code. Each re-run is named in the
output and recorded as `retried`.

A step whose command starts with `pnpm exec <bin>`, `npx <bin>` or `bunx <bin>` runs
`node_modules/.bin/<bin>` when it exists, and every step runs with
`pnpm_config_verify_deps_before_run=error` unless the environment or `pnpm-workspace.yaml` already
says otherwise: a gate step must not install into `node_modules` while it measures it.

## `--affected`, and the mark each tier keeps

```bash
geonosis verify fast --affected       # diffs against refs/geonosis/verified
geonosis verify release --affected    # diffs against refs/geonosis/verified-release
```

Each tier keeps its own git ref: `refs/geonosis/verified` for `fast` (the name consumers and the
`lint-config` check already read), `refs/geonosis/verified-<tier>` for every other tier. A tier moves
only its own mark, to HEAD, and only when every step it ran was green and no lint input differs from
HEAD; a red run, or another tier's green run, leaves it where it was.

`--affected` runs a `per: "workspace"` step over the workspaces changed since the merge-base of HEAD
and that tier's mark — committed and uncommitted changes alike — and every workspace whose manifest
depends on one of them. A changed path in no workspace selects them all and runs every test, and
the run says which path did it: nothing says what a root file, a lockfile, a config, a fixture or a
script outside the workspaces reaches. A path the top-level `"documents"` globs of `geonosis.json`
name (default `["**/*.md", ".changeset/**"]`) selects nothing, so a change of documents only skips
the runtime tests and says so; a batch still runs its `also` paths. A repo whose tests read a
document names narrower globs. `--since <ref>` diffs against that ref instead. With no
mark yet, `fast` diffs against `main` and every other tier runs every workspace, so a tier's first
green run covers the whole tree and then sets its mark. The second run over a tree its mark already
names runs no workspace; a batch step still runs its `also` paths.

A batch step that declares `"graph": "vitest"` is narrowed further by Vitest's own import graph.
`--affected` hands the batch `--changed --experimental.vcsProvider=<@geonosis/verify's
dist/vitest-changed.js>`, and the provider answers Vitest with the files verify read as changed:
the changed sources, every git-ignored file under an affected workspace (its build, which a
dependent's tests import), and every test file under the step's `also` paths, which ride whole.
Vitest runs the tests whose imports reach one of them. The step runs whole, saying why, when a
changed source is not a module the graph follows (a `.json` fixture, a snapshot, a manifest), since
a test could read it with `fs` where no import shows it; when a changed path is in no workspace;
and when a changed module is gone from the tree, deleted or renamed away: Vitest 5's graph holds
only files that exist, so a test whose import no longer resolves is not reached from the path it
imported. A rename is a change at both of its paths. Vitest's `No affected test files found` and
`No changed files found` are read as a step that ran nothing.

The kit's own `.geonosis/` directory is never a change, as the tree hash never reads it, so a run's
own report does not select every workspace on the next one.

A change the version cut writes is no change: a `package.json` whose only difference from the base
is its `version`, any `CHANGELOG.md` or `GROUP-CHANGELOG.md`, and each path in the top-level
`"versionFiles"` list of `geonosis.json` whose only difference is its version numbers. A cut that
bumps every manifest selects no workspace; a dependency range or a script that moved beside the
version still does.

A tier that goes green also writes `.geonosis/green-<tier>.json`: the hash of the tree it ran on,
HEAD plus every uncommitted and untracked change, as `treeHashOf` reads it. `greenOnThisTree(root,
tier)` answers that record only while the tree hashes the same, so `release:check` does not run
`verify:release` again over a tree it was green on. No record is written by a red run, by a run
limited with `--since`, or by one whose steps changed the tree they ran on.

### `green record` and `green check` — a tool's own green

```bash
geonosis verify green record <name> [--asked <text>]
geonosis verify green check <name> [--asked <text>]
```

For a tool that keeps a green of its own, such as `geonosis update`'s resume. `record` writes
`.geonosis/tool-green-<name>.json`: the tree hash, and the question `--asked` names. `check` prints
the record and exits 0 while the tree hashes as it did and the question matches, and exits 1
otherwise. A name that is not one path segment is refused. No tier, Stop hook or release check
reads the record, so `green record fast` cannot turn a red tier green: a tier is green only by a
run of its own. Typed in a Claude Code session, `green record` is a door the person opens: the
plugin's Bash hook asks before it runs, naming the record, the tree it is written over and what it
lets a tool skip. `geonosis update` writes its own records through the binary and is not asked.

Every step sees `GEONOSIS_BASE`, the commit that tier's mark is merge-based to, unset until there is
one; a task turbo runs sees it only when `turbo.json` declares it (see the turbo step above).

`--require-work` fails a tier when every step marked `"runner": true` exited 0 saying it ran nothing
(turbo with no affected task, a runner with no test file, a command that printed `SKIP`) and the
diff since the mark holds a code file that came with no test of its own. A diff of documents only,
and a tree already at its mark, skipped nothing and pass; a tree no diff can be read from fails,
since nothing there says what was skipped.

## The report

`.geonosis/gate-report.json`, unless `--report` says otherwise:

```jsonc
{
  "tier": "full",
  "startedAt": "2026-08-30T10:00:00.000Z",
  "finishedAt": "2026-08-30T10:00:04.500Z",
  "ok": false,
  "steps": [
    { "id": "fast/1", "command": "pnpm typecheck", "ok": true, "exitCode": 0, "ms": 2140, "tail": "…", "unproven": true },
    { "id": "fast/2", "command": "pnpm lint", "ok": false, "exitCode": 1, "ms": 1830, "tail": "…" },
    { "id": "full/1", "command": "pnpm build", "ok": false, "exitCode": -1, "ms": 0, "tail": "", "skipped": true }
  ]
}
```

- **`id` is `<tier>/<n>`, numbered within the tier that DECLARED the step** — not within the run. An
  inlined step keeps its own tier, so `fast/2` is the same command whether the operator ran `fast`
  or ran `full`. Numbering per run would renumber every fast step the moment `full` grew one in
  front of it, and an id that moves is an id nothing can compare against yesterday's report.
- **The run stops at the first red, and the rest are still written down**, as `skipped` with
  `exitCode: -1`. A report that silently lost the tail of the tier reads as a shorter, greener tier.
- **`tail` is the last 40 lines of stdout and stderr, interleaved**, with the colour codes taken
  out. The shell folds the two streams, so they stay in the order they were written; concatenating
  two captured buffers would not.
- Each step runs in a **subshell**. `<step> 2>&1` binds the redirect to the last command of the step
  only, so `echo boom >&2; exit 3` would lose "boom" entirely — and a gate whose report is missing
  the error line is a gate nobody can act on.
- **`timedOut` is `{ loadAverage, cores }`**, recorded on a red step whose output carries a runner
  timeout message (vitest `Test timed out in` / `Hook timed out in`, jest `Exceeded timeout of`,
  bun `this test timed out after`). The run prints `timed out at load average X on N cores` under the
  FAIL; the verdict stays FAIL.
- **`unproven: true` is a step that declared no `proves`** — a gate nobody has ever watched fail. It
  is recorded on every run and blocks nothing.
- **`log` is the whole output of a red step**, kept in `.geonosis/logs/<tier>/<id>.log` (the id's
  `/` written `-`, and after its `@` a `/` written `%2F` and a `%` written `%25`, so
  `packages/a-b` and `packages-a/b` keep two logs) and printed under the FAIL as `full output: <path>`. The tier's directory is
  emptied at the start of each run, and an output past 4 MB keeps its first and last lines with the
  number of bytes dropped between them. `tail` is a window of 40 lines; the log is what a refusal
  is read from when the window missed the cause (#159).
- **A step's `failureMark`** is a pattern (a regex source) its own failure lines carry: vitest's
  `" FAIL "`, a mutation lane's `"^(?:survived|unjudged)\\s"`. A red step's `tail` then opens on the
  first line it matches, not on the first line that looks like an error, which a passing line that
  quotes an `Error:` owns. A mark that matches no line leaves the window where it opened before.

## The budget a tier declares, and the CI job's own timeout (#161, D-042)

```jsonc
{
  "verify": { "fast": ["pnpm typecheck", "pnpm lint", "pnpm test"], "full": ["fast", "pnpm ratchet"] },
  "budgets": { "fast": 150000 }
}
```

Milliseconds per tier, optional, and a budget naming a tier nothing declares is **refused** — a
ceiling nothing can be measured against is a ceiling that cannot fire. Over budget prints one WARN
line naming D-042 and the tier's verdict is **unchanged**: a budget is a ceiling a repo chose, never
a definition of done. A budget that could fail a tier is a number people raise instead of read. The
report carries `budgetMs` beside the per-step `ms`, so a CI step reads the timings against the
ceiling without re-reading the config.

A budget is not a timeout. It is measured after the run, so it says nothing at all about a job that
never finishes — one consumer's CI browser job ran **three hours** before failing, and every minute
of it was billed to a gate nobody could see. Every job that runs a tier declares its own ceiling:

```yaml
jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 30     # the runner's ceiling; the budget is the repo's
    steps:
      - run: npx geonosis verify full
```

## A scoped fast tier needs a total full tier (#136)

Changed-files linting is what makes a fast tier fast, and it is how a hard cap goes unwatched:
every hard cap needs a full-tier counter, or it is a cap in prose. The rule, and the incident behind
it, are the "pairing rule" of [`@geonosis/ratchet`](https://www.npmjs.com/package/@geonosis/ratchet),
whose counters are that full-tier check.

## What a cached runner can hide

A step like `pnpm test` that a build runner (turbo, nx) satisfies from cache is green by REPLAY: 50
cached tasks answered a Medusa storefront's full tier while a live, race-caused failure sat in the tree, and
only `--force` surfaced it. The cache is sound for deterministic tests — but a verify step
satisfiable from cache means a flaky test's red can be permanently masked by its own earlier green.
If a tier exists to catch non-determinism, its step must bust the runner's cache
(`pnpm test -- --force`, or the runner's equivalent) and pay the time; a cached gate proves the
tree compiled once, not that it passes now (#128).

## `stats` — the first-try pass rate

Not a gate; the KPI a law diet is judged by. The Stop hook records every turn-end it gated in
`.geonosis/stop-turns.json`, one bucket per day, and this reads it back:

```bash
geonosis verify stats
geonosis verify stats --since 2026-09-07     # that window, and everything before it
geonosis verify stats --json
```

```
first-try pass rate — the share of turn-ends whose fast tier was green
  before          61 turns ·     18 blocked · 70.5 % first try
  2026-09-07+     58 turns ·      6 blocked · 89.7 % first try
```

A turn whose fast tier was green on the first run is a turn where the code was written the way the
gates accept without being told twice. A blocked turn is a correction loop. `--since` gives the two
windows a before/after needs, from one command and one instrument — the procedure is
[`docs/law-diet.md`](../../docs/law-diet.md).

Three refusals, all of them the same refusal: **an empty record is not a perfect score.** A repo
where the hook has never run says "no turns recorded" and gives no rate; a record that will not parse
is an error, not zero turns; and a turn the hook never gated (`stop_hook_active`, where the gate does
not run at all) is not counted, because there was no first try to have.

The sibling `stop-blocks.json` cannot answer this and is not asked to. It is the block cap's working
memory, and a green turn deletes its own row from it.

## `lint-config` — the lint a tier runs may not get weaker unasked

```bash
geonosis verify lint-config                          # a step in a tier: pnpm exec geonosis verify lint-config
geonosis verify lint-config --accept-config-change   # records the approval; the person allows it at the permission prompt
```

It finds every lint command of the repo (a `package.json` script of any workspace, a `geonosis.json` verify command that runs `oxlint`) and, against the same command at `refs/geonosis/verified`, judges what the lint DOES:

- the **effective config**: `oxlint --print-config` with the command's own `--config`, `-A`/`-W`/`-D` and plugin flags. A rule whose effective level drops or that is gone, a plugin gone, an option changed, a rule kept at error with looser options fail; a new rule at error, a level raised, an ignore pattern removed, a new override of error rules and a plugin added pass.
- the **command**: a target no longer linted, a flag removed or added, anything around oxlint changed (`|| true`, `; exit 0`, a pipe), the command gone, its config unreadable.
- the **findings**, when anything differs: the mark's configs laid over a copy of the tree against the tree's, each run the way the command runs oxlint over the mark's targets, plus a plugin's reach corpus (`@geonosis/lint-parity`). A diagnostic the mark produces and the tree does not fails, naming rule and file, so a nested `.oxlintrc.json` that drops rules shows up.
- the **mark**: the gate records that it existed; a repo that had one and lost it fails.

The approval is the person's consent, given through Claude Code's permission prompt: the FAIL tells the agent to run `<runner> geonosis verify lint-config --accept-config-change`, the `pre-bash` hook of the geonosis plugin asks the person to allow it (listing the changes in plain words), and Allow records them in `.geonosis/lint-config-approval.json`. With no mark, or with no command that runs oxlint, it passes and says so.

## `--exclusive`

Takes the machine-wide heavy lock `@geonosis/gate` owns, so a verify and a ratchet on one laptop
serialise against each other rather than each against itself.
Three sessions each starting a full gate is how a 25-minute run becomes two hours and produces
failures that are about the load and not the code.

The lock is taken only AFTER every refusal has been decided: a run that waits half an hour for the
machine and then finds the tier was misspelled has held it against two other sessions for nothing.
`--exclusive-timeout <seconds>` bounds the wait (default 1800) and says who it is waiting for.

## A tier runs once per tree: `--fresh`, and the gate daemon

A tier that went green records the tree it ran on and the flags it ran with
(`.geonosis/green-<tier>.json`). The same tier asked again of the same tree with the same flags is
answered from that record, in milliseconds, and runs nothing:

```
  green  fast already green on this tree 3f2a91c0d4e7 since 2026-10-14T10:12:03.000Z (--affected --require-work) — nothing re-run; --fresh runs it again
```

A tree differs when HEAD, any tracked change or any untracked file differs, or when the install
changed as npm, pnpm or yarn records it under `node_modules`. So an edit or an install always runs the
tier again. bun writes no such record, and its tracked `bun.lock` stands for the install. A tier whose
verdict also depends on environment variables names them under `keyEnv` in `geonosis.json`
(`"keyEnv": ["DATABASE_URL"]`), and a change in any of their values runs it again. A git-ignored
file a tier reads, such as a Worker's `.dev.vars`, is named under `keyFiles` (`"keyFiles":
[".dev.vars"]`; a path outside the repo is refused): its content is hashed into the same key, so a
changed file runs the tier again and an unchanged one keeps the recorded green. The gate report
records that key as `inputs`, and the geonosis plugin's Stop hook computes it too: it reads a green
report, a judged tree or a daemon's green only while the `keyEnv` values and `keyFiles` contents
are the ones it ran under, and hands the daemon the key verify asks under, so a changed `.dev.vars`
runs the fast tier again at the next turn end. A red is never recorded, so a red tier always runs
again. `--fresh` runs the tier whatever is recorded.

A `batch` step with `counts: "vitest-json"` also keeps the files it knows green
(`.geonosis/known-green-<tier>-<step>.json`): every file the report passed, green run or red, with
the commit and the content of each uncommitted path it ran over. The next run passes the files still
green to the runner as `--exclude`, so a red run followed by a fix runs the files that failed and
what the fix reached, not the whole step again. A file stops being green when it changed, when a
non-test file in its workspace or in a workspace it depends on changed, or, for every file, when any
non-test file in no workspace changed: a lockfile, a config, a patch, a path under `also`, a document
a test reads. Nothing says which tests read such a file, so none is kept. The verdict line counts the
files it did not run: `verify PASS — fast — … (2 test files known green, not run)`. `--fresh` runs
every file.

With a `geonosis-gate` daemon running (`@geonosis/gate`), `geonosis verify <tier>` asks it first:
the tier runs once for every verify over the same tree and flags. A verify over a tree the daemon
holds a green for is answered that green, with when and at what load it was made:
`gate  answered from the green kept at 2026-10-15T17:18:59.000Z (3 min ago, load 4.1)`. The daemon
keeps no red. A red can come of the machine or the environment, so once the run that made it is over,
the next verify runs the tier again. `--exclusive` never asks the daemon: it takes the machine-wide
lock itself.

## Programmatic use

```ts
import { loadVerifyConfig, resolveTier, runSteps, shellRun, writeReport } from '@geonosis/verify'

const steps = resolveTier(loadVerifyConfig(cwd), 'fast')
const report = runSteps({ run: shellRun(cwd), steps, tier: 'fast' })
writeReport(cwd, '.geonosis/gate-report.json', report)
```

## What this cannot see about itself

- **It does not know whether your steps measure anything.** `verify` reports what your commands
  exited with; whether `pnpm lint` was pointed at the right directory is between you and the
  ratchet's `--prove`.
- **`.geonosis/` is runner-owned.** The `geonosis` Claude Code plugin refuses agent writes there
  (D-007) — the scored agent never writes the scoreboard. Nothing in this package enforces that.
