@geonosis/verify
v3.0.0
Published
One gate runner — tiers of shell steps, run serially, recorded as a JSON gate report.
Maintainers
Readme
@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
pnpm add -D @geonosis/verify # bun add -d, npm i -D--prove is the first thing to run after installing
geonosis-verify --proveIt 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 1A 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:
{
"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.
proves: how a step can be made to fail
A step may declare its own falsification beside its command:
{
"verify": {
"fast": [
"pnpm typecheck",
{
"command": "pnpm lint",
"proves": {
"plant": "printf 'debugger\\n' > src/planted.ts",
"expect": "no-debugger"
}
}
]
}
}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 provenEach 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.
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. 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
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.
The report
.geonosis/gate-report.json, unless --report says otherwise:
{
"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 }
]
}idis<tier>/<n>, numbered within the tier that DECLARED the step — not within the run. An inlined step keeps its own tier, sofast/2is the same command whether the operator ranfastor ranfull. Numbering per run would renumber every fast step the momentfullgrew 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
skippedwithexitCode: -1. A report that silently lost the tail of the tier reads as a shorter, greener tier. tailis 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>&1binds the redirect to the last command of the step only, soecho boom >&2; exit 3would lose "boom" entirely — and a gate whose report is missing the error line is a gate nobody can act on. unproven: trueis a step that declared noproves— a gate nobody has ever watched fail. It is recorded on every run and blocks nothing.
The budget a tier declares, and the CI job's own timeout (#161, D-042)
{
"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:
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 30 # the runner's ceiling; the budget is the repo's
steps:
- run: npx geonosis-verify fullA scoped fast tier needs a total full tier (#136)
Changed-files linting is what makes a fast tier fast — and it is exactly how a hard cap goes unwatched: two max-lines caps were breached in a Medusa storefront with nothing noticing, because the cap was a finding only a tool the full tier never ran could see, and a file over a cap sits there until someone happens to edit it. The pairing rule: every hard cap needs a FULL-TIER counter, or it is a cap in prose. The fast tier is scoped, therefore the full tier must be total.
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:
geonosis-verify stats
geonosis-verify stats --since 2026-09-07 # that window, and everything before it
geonosis-verify stats --jsonfirst-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 tryA 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.
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.
--exclusive
Takes the machine-wide lock @geonosis/ratchet owns, through that package's published entry, 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.
Programmatic use
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.
verifyreports what your commands exited with; whetherpnpm lintwas pointed at the right directory is between you and the ratchet's--prove. .geonosis/is runner-owned. ThegeonosisClaude Code plugin refuses agent writes there (D-007) — the scored agent never writes the scoreboard. Nothing in this package enforces that.
