# The geonosis group

One number versions every package of the fixed group. A package changelog lists only what a changeset named for that package, so this file lists what the number bought: every sentence any member carries, once, with the members that carry it.








## 3.7.0

- `@geonosis/base/node` exports `treeHashOf`, `treeFilesOf`, `filesThatMoved`, `isolatedGit`, `withoutGitRedirects`, `GIT_IDENTITY`, `answerInformational` and `informationalAsk`, moved from the ratchet's source so a package below the ratchet can use them (#215, plan 049 T5). `@geonosis/ratchet` still exports every one of them, unchanged. (base, ratchet)
- JSONC is read by Microsoft's `jsonc-parser` in place of the comment-and-comma stripper `@geonosis/base` carried, and a wrangler config of any extension is read by wrangler's own config reader (`experimental_readRawConfig`), as a `wrangler.toml` already was:  - **Refused now, as oxlint and wrangler refuse it:** a comment between two tokens, such as `1/* why */0`, which the stripper joined into `10`. The doctor, the ratchet's counters, lint-parity, the MCP preamble, `geonosis explain`, `create-geonosis --plan`, the evals scorer and `deriveLocalOnlyConfig` name the line of what they could not read. - **Read now:** a config whose lines end in a carriage return alone, which wrangler and TypeScript read and the stripper refused; the lint plugin reads such a `tsconfig.json`'s `paths`. - **`geonosis explain`** refuses, naming the file, when a file it reads does not parse, where it answered with defaults. - **New:** `@geonosis/ratchet` and `@geonosis/cli` export `parseJsonc`, which throws a `SyntaxError` naming the error and its line. - **Removed:** `@geonosis/base: parseJsonc` (`@geonosis/base` takes no dependency) and `@geonosis/release: parseJsonc`. Import `parseJsonc` from `@geonosis/ratchet`, or depend on `jsonc-parser`. (base, cli, doctor, evals, lint-parity, mcp, oxlint-plugin-biological-architecture, ratchet, release, testbed, create-geonosis)
- A tool run through the door prints its usage the way it was run: `geonosis ratchet --help` says `geonosis ratchet [--cwd <dir>] …`, not `geonosis-ratchet …`, and the same for every tool the door fronts. The door tells the tool in `GEONOSIS_DOOR`, naming the bin it started, so a tool that tool starts in turn keeps its own name. `@geonosis/base/node` exports `doorSpelled`, `doorEnvFor` and `DOOR_ENV`. A tool run by its own bin prints its own name, as before (#68). (base, cli, doctor, ledger, lint-parity, observability, ratchet, release, review, testbed, verify, verify-arch, visual-diff, walk)
- The fast tier `geonosis init` and `create-geonosis` write records each counted tool as `pnpm exec geonosis ratchet --record <counter>` (`bunx geonosis ratchet …` on bun), through the door every repo that installs `geonosis` has. Under pnpm `pnpm exec geonosis-ratchet` was `Command "geonosis-ratchet" not found` unless `@geonosis/ratchet` was a direct dependency. `geonosis post-update`, which `geonosis update` runs, moves the record steps an older init wrote to the same spelling, leaves every other byte of geonosis.json as it was, names each step it moved, and moves nothing on a second run (#68). (base, cli)
- `geonosis init` measures `typescript/no-explicit-any` for `frontend` (#158), and with `i18n` the react plugin's rules that catch real bugs (`jsx-key`, `iframe-missing-sandbox`, `jsx-no-script-url`, `refs`, `set-state-in-effect`, `jsx-no-duplicate-props`, `jsx-no-undef`, `no-children-prop`, `no-danger-with-children`; #195) before it enforces them. A rule that finds nothing is written at `error`. One that finds something is parked at `off` beside an `oxlintRule` counter `census:<rule>` that may only shrink, whose number the ratchet adopts at its first run; `geonosis ratchet --prove` plants a finding for it. `geonosis update` adds the same census to a repo that lacks it, through a new `census` post-update step: it asks oxlint what `.oxlintrc.json` resolves each rule to (`oxlint --print-config`), leaves a rule that resolves to on — under any spelling, at any level, through `extends`, an override or a category — and measures the others as init does, writing a rule that finds nothing at `error`. A config oxlint cannot resolve fails the step, and nothing is written. The plugin exports `censusOf` and `CENSUS_PRESETS`, the one list both read, from its root and from `./presets`.  Why `any` is measured at all: removing 475 explicit `any`s from one storefront found five live bugs it had hidden, among them a price sort subtracting a field variants do not have, a review list reading `id` off deleted reviews, and a misspelled relation the backend ignored without an error. (cli, oxlint-plugin-biological-architecture)
- `npxTool(id)` is exported: the `npx --yes --package @geonosis/<id> geonosis-<id>` command the Python law names its gates by. `create-geonosis --language py` writes its CI steps, its `full` tier and the line it prints after scaffolding from it, so the law and the tree it ships beside cannot name a gate two ways. The files it writes are unchanged. (cli)
- `geonosis update` prints one `young` line for a release family: versions the lockfile gained that share a version and a repository (the registry's `repository` of that version, normalized, directory left out) (21 lines for `oxc-parser`, its 19 platform bindings and `@oxc-project/types`) become one line with the count, the repository, the version and the youngest publish time. `--verbose` lists each package, and `.geonosis/bump-report.json` keeps every one under `young` (#206). (cli)
- `geonosis update` runs the doctor's checks before the repin with the copy installed then, and again after the install. Each WARN or FAIL present after and absent before, told apart by check and subject and not by message text, is a BY HAND item naming the check and its message, and is kept in `bump-report.json` under `byHand`. A rule the `loaded` check already lists as needing an option is not repeated; when the first run gives no findings report, the output says so and quotes the tail of what it answered. The doctor runs against the `home` the update was given (#198). (cli)
- A re-run of `geonosis update` does not repeat the install, the frozen lock, the doctor or post-update when each was already green over the tree the re-run finds. Each is recorded after it passes with the mechanism `geonosis-verify` records a tier with (`.geonosis/green-update-<step>.json`, the tree hash and the way the bump was asked); the re-run resumes after the latest step whose record matches, shows those steps as `resumed`, and carries the doctor's BY HAND items from the last report. `--fresh` runs every step again. A verify or build step that timed out names the steps that had passed and says a re-run resumes after them (#186). (cli)
- `geonosis update` writes each step's start and end, with its time and outcome, to stderr as it goes, and one line a minute naming the step and the time elapsed while a step runs. A bump that spent 23 minutes in an install or a test step is no longer silent for them. The lint step runs its child synchronously, so it prints its start and end and no line between (#166). (cli)
- The AGENTS.md that `sync --target agents-md` writes names the command that regenerates it as `geonosis ledger sync --target agents-md`, the spelling every consumer has; under pnpm `geonosis-ledger` is a command only where `@geonosis/ledger` is a direct dependency (#68). The new header makes `sync --target agents-md --check` fail on an AGENTS.md an older ledger wrote, so `geonosis post-update`, which `geonosis update` runs, regenerates an AGENTS.md whose header says the ledger wrote it, and leaves any other AGENTS.md as it is. (cli, ledger)
- The `geonosis` door prints a tool's stdout on its stdout and the tool's stderr on its stderr. It used to print both on stdout, after its own note that it runs the copy that came with `@geonosis/cli`, so `geonosis doctor --json` printed text around the report and a reader of stdout could not parse it. The note goes to stderr. `geonosis --json <command>` still joins both in `output`, and `runBin` still returns them joined. (cli)
- `geonosis <tool>` for a tool the door fronts (ledger, ratchet, verify, doctor and the rest) finds and starts the tool without loading the whole command registry; the registry is imported only for the door's own commands and its usage. Measured on bun 1.4.3 at load 19–20, `geonosis ledger delivery check --cached` took a median 379 ms against 139 ms for the ledger bin before, and 204 ms against 125 ms after; the door's own CPU went from about 190 ms to about 38 ms. The commit and push guards run the ledger through the door in a repo that installs only `geonosis`, inside a 2.5 s budget (#215, #68). (cli)
- A command that fronts a bin finds the repo's own copy of the tool in the `node_modules` of the repo, from the directory it runs in up to the repo root (the nearest directory holding `.git`, `pnpm-workspace.yaml` or a `package.json` with `workspaces`, else the nearest holding a `package.json`), read off the filesystem, and otherwise the copy that came with `@geonosis/cli`, found from the cli's own location, and says so. Through the resolver, the repo lookup also took the pnpm virtual store that a pnpm bin shim puts on `NODE_PATH`, and on bun its global install cache, and a copy in a `node_modules` above the repo, so the cli's or another version's copy ran without the note. A repo's copy that declares a bin and no entry point is now found too (#68). (cli)
- The full tier `geonosis init` writes ends with `pnpm exec geonosis ratchet` (`bunx geonosis ratchet` on bun), through the door its record steps use, where it ended with `pnpm ratchet` over a root that declares no `ratchet` script; a root that declares one keeps `pnpm ratchet`. (cli)
- The rule files, skills, commands and agents spell the ledger `geonosis ledger …`, the command the metapackage links under every package manager; under pnpm `geonosis-ledger` is a command only where `@geonosis/ledger` is a direct dependency. The rule files `geonosis rules` copies and the `todo-names-its-plan` fix shape say it that way. The `@geonosis/cli` README says what a pnpm consumer installs directly: `geonosis` or `@geonosis/cli` for the door, which the plugin's hooks, the commit guard included, run where the repo links no tool of its own. The `geonosis` README no longer says the package ships no bin (#68). (cli, doctor, oxlint-plugin-biological-architecture, geonosis)
- A message that tells a person what to run names it through the `geonosis` door: the baseline write refusal and the ratchet's FAIL name `geonosis ratchet --raise …`, an unproven fall `geonosis ratchet --bank …`, a missing or stale recording `geonosis ratchet --record …`, the session doctor `geonosis doctor`, the Stop hook, `geonosis verify` and the doctor's `runner` check `geonosis verify prove`, the ledger's handoff check `geonosis ledger handoff`, its push guard `geonosis ledger land --attest-only`, a plan envelope's next step `geonosis ledger plan check`, the release's smoke and migrations messages `geonosis release …`, each tool's `--help` and `--print-config-shape` pointers `geonosis <tool> …`, the write guard on `.geonosis/` `geonosis verify fast`, and init and post-update's census line name `geonosis ratchet` and `geonosis verify` for the gates init writes. Init no longer asks the root to declare `@geonosis/ratchet` and `@geonosis/verify` for their bins: its gates call the door. Under pnpm a tool's own bin is a command only where its package is a direct dependency (#68). (cli, doctor, ledger, observability, ratchet, release, verify, geonosis)
- `geonosis post-update` regenerates every `AGENTS.md` in the tree whose header says the ledger wrote it (`docs/AGENTS.md`, a package's own), outside `node_modules` and dot directories, beside the one `ledger.agents` names. It used to read only the root one, so a `docs/AGENTS.md` an older ledger wrote kept failing its `--check` after the update. An `AGENTS.md` the ledger did not write is still left as it is. (cli)
- `geonosis post-update` names each tier command it leaves running the ratchet by its own bin (`geonosis-ratchet --record bare`, `./node_modules/.bin/geonosis-ratchet …`, `pnpm exec geonosis-ratchet --tier fast`), by its place in `geonosis.json`, and says it does not rewrite a spelling no older init wrote. A run that moved nothing used to say every tier command already runs `geonosis ratchet`, which was false while such a step was there. (cli)
- The law a Python repo starts with names its gates as the commands that work there: `npx --yes --package @geonosis/verify geonosis-verify fast` (and `full`) and `npx --yes --package @geonosis/doctor geonosis-doctor`, as its CI and `geonosis.json` run them. A uv repo has no `node_modules`, so the bare `geonosis-verify` it named was on no PATH. (cli)
- `geonosis update` counts the oxlint findings of a plugin bump over every oxlint config that loads the plugin, an `oxlint.config.ts` or `oxlint.config.mts` and a config a lint command names with `-c` included, where it used to count `.oxlintrc.json` files only. A config a lint command names is run over the paths that command lints, as the doctor's `downgrades` runs it, not over the config's own directory. A config in the tree that cannot be loaded makes the lint step fail naming it, rather than leaving it out of the count (#169). `@geonosis/doctor` exports `namedConfigTargets`, the configs the repo's lint commands name and the paths each lints. (cli, doctor)
- `geonosis update --registry <dir>` run a second time over the candidate it installed lists the changelog entries above the version the tree came from, read off the last commit, where it used to read the `file:` pin it had written as no version and list every entry back to the first release. A `file:` pin the last commit holds is read as the version in its tarball's `package.json`, so a committed candidate lists none. (cli)
- `geonosis update` and `geonosis post-update` name the commands their steps ran as the door spells them: `geonosis doctor --only …`, `geonosis verify <tier>`, and `geonosis doctor` as the reader of `.geonosis/post-update.json`, in their reports and their `--help`, and `geonosis update --help` names `geonosis release smoke` as what writes a `--registry` directory. `geonosis sync --help` says a `--target` goes to `geonosis ledger sync`. Each named the tool's own bin, which under pnpm is a command only where its package is a direct dependency (#68). (cli)
- `geonosis update --registry <dir>` under a lifted release-age gate names the young third-party versions the candidate brought in, asking `--npm-registry` (npm by default) when each was published, where it named none: the young check was skipped whenever the kit came from a directory. A version the registry gives no date for is a `young?` line, as on any other bump. (cli)
- `geonosis-doctor` asks a new question, `census` (#158): for every `oxlintRule` counter in `geonosis.ratchet.json`, does the config it reads run its rule? It asks oxlint what that config resolves to (`oxlint --print-config`), and reads a JS plugin's rules, which `--print-config` leaves out of its answer (oxlint 1.88.0), from the config and the configs it `extends`; a rule id is a JS plugin's where oxlint refuses it in a config with no `jsPlugins`. It FAILs a counter whose rule no layer names, or whose plugin `"plugins"` leaves out: such a counter reads 0 whatever the tree holds, and the ratchet banks the 0. It FAILs too a rule switched off where the counter names no `"config"` to raise it, and one switched off only in a config this one extends, which the counter does not raise. A census `geonosis init` or `geonosis update` wrote, its rule parked at `"off"` in the config it names, is OK. (doctor)
- A `turbo-env` check (#215 follow-up): a per-workspace verify step that names a turbo task is held against the env its `turbo.json` or `turbo.jsonc` declares for that task (`globalEnv`, `globalPassThroughEnv`, `global.env`, `global.passThroughEnv`, the task's and each package task's `env` and `passThroughEnv`, and each workspace's `turbo.json`). turbo's default strict env mode hands a task only what these declare, so a task that declares none of `GEONOSIS_BASE`, `GEONOSIS_GATE_RUN`, `GEONOSIS_HEAVY_SLOT_HELD` and `GEONOSIS_SLOTS` is WARNed, naming the step, the task and the keys to add: `"passThroughEnv": ["GEONOSIS_*"]`, and `"env": ["GEONOSIS_BASE"]` for a task that selects by the base. Loose env mode is OK; no turbo step, or no `turbo.json`, is a SKIP; a config that cannot be read is UNJUDGED. (doctor)
- The `workers` check compares a committed `worker-configuration.d.ts` with its wrangler config by running `wrangler types --check`. It passes the flags the file's own header records, because wrangler 4.151.0's `--check` ignores them (proofs/076). Types the config has moved away from FAIL, naming the command that regenerates them. A run that gives no verdict is UNJUDGED (plan 046 F3). (doctor)
- `downgrades` no longer WARNs a rule an oxlint config parks at `"off"` when an `oxlintRule` counter in `geonosis.ratchet.json` raises it in that config and counts it (#158): that is a census that may only shrink, and the line is OK, naming the counter and its baseline number. A rule at `"off"` that no counter counts is WARNed as before. (doctor)
- A run asks its checks in the order `geonosis doctor --help` lists them, the check registry's: `drift` before `observability` and `workers`, `rules` before `downgrades` and `turbo-env`. The run kept a second, different order, which `ran` in the JSON report followed. (doctor)
- The doctor's census check reads a counter's rule spelled with its plugin as the ratchet reads it: `biological-architecture/atom-no-deps` counts that plugin's rule, `eslint/no-debugger` counts the built-in one, and a bare name counts the rule of any plugin. A counter spelled `plugin/rule` over a config that runs it was FAILed as reading 0 while the ratchet counted its findings. (doctor)
- `geonosis doctor --help` says what the `workers` check does with a committed `worker-configuration.d.ts`: it holds it to its wrangler config with `wrangler types --check`, run with the flags the file's header records, FAILs types the config moved away from and is UNJUDGED on a run with no verdict. The help left that half of the check out. (doctor)
- The doctor's runner check counts only a ratchet run that carries `--prove`: `geonosis ratchet` or `geonosis-ratchet` must be the command a step runs, alone or behind `npx`, `bunx`, `bun x`, `pnpm exec`, `pnpm dlx`, a path or an environment assignment. `geonosis ratchet --tier fast && echo geonosis ratchet --prove`, and a script that only echoes it, used to read as proving the ratchet. (doctor)
- `geonosis-doctor` reads every oxlint config oxlint would load, not only `.oxlintrc.json`: each `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts` and `oxlint.config.mts` in the tree, and every config a lint command names with `-c` or `--config` (a script of any workspace, or a `geonosis.json` tier step), whatever it is called. A `.ts`, `.mts` or `.js` config is imported the way oxlint imports it and its default export read, so `loaded`, `exercised`, `downgrades`, `drift` and `group` judge the rules it enables; one that cannot be loaded is a `loaded` FAIL and a `downgrades` UNJUDGED naming why, where it used to be skipped or, under `downgrades`, to stop the run with a JSON parse error. `downgrades` judges each lint step with the oxlint nearest the directory the step runs in (`binNear`), so a pnpm workspace whose lint steps each install their own oxlint is no longer UNJUDGED "no oxlint is installed above <root>". `discoverConfigs` is now async and takes the workspaces; `loadConfig` and `OXLINT_CONFIG_NAMES` are exported, and `DiscoveredConfig` carries the loaded object as `declared` (#169, #170). (doctor)
- The doctor recognises a ratchet step spelled through the door, `geonosis ratchet --tier fast`, as `init` now writes it, however the door is run: `pnpm exec`, `npx` (with its flags), `pnpm dlx`, `bunx`, a path to the bin, and a pinned spec such as `geonosis@3.7.0`. Before, its own-tier timing check saw no ratchet there. The `runner` check counts the ratchet as proven only where a ratchet run, direct or through a script, carries `--prove`, where any `--prove` anywhere counted, `echo --prove` included; it advises `geonosis ratchet --prove` (#68). The same reading of a command's name, a path, `.mjs` and a pinned `@version` stripped, serves the `once` check (a tier running `npx geonosis@3.7.0 ratchet`, or `npx tsc@5.9.3`), `stack.lint` (`npx -y oxlint@1.88.0 .` is oxlint) and the `census` check's reading of the config a counter's command names (`npx oxlint@1.88.0 --config …`). (doctor)
- `turbo-env` judges the task turbo runs in each workspace that has the step's script: the root's `<name>#<task>` entry where there is one, which replaces the generic task, else the generic task, with the workspace `turbo.json`'s `env` and `passThroughEnv` replacing the root's or extending them after `$TURBO_EXTENDS$` (measured on turbo 2.11.8). The step is OK only when every such workspace's task declares a `GEONOSIS_*` variable, and a WARN names the workspaces that do not. One package's declaration made the whole step OK while another package's task saw none of the variables. (doctor)
- New `@geonosis/gate`: the heavy slots, the machine-wide heavy lock and the `geonosis-gate` daemon, moved out of `@geonosis/ratchet` with their behaviour and tests unchanged (#215, plan 049 T5). It joins the fixed group; `geonosis-gate serve`, `status` and `stop` are its bin now, and a repo that starts the daemon adds `@geonosis/gate` by name.  `@geonosis/ratchet` no longer ships the `geonosis-gate` bin and re-exports none of what moved. Import these from `@geonosis/gate` instead: `@geonosis/ratchet: acquireExclusive`, `@geonosis/ratchet: availableMemoryMb`, `@geonosis/ratchet: buildLockPath`, `@geonosis/ratchet: DEFAULT_MEMORY_MB_PER_SLOT`, `@geonosis/ratchet: describeBudget`, `@geonosis/ratchet: describeHolders`, `@geonosis/ratchet: GATE_PARENT`, `@geonosis/ratchet: GATE_RUN`, `@geonosis/ratchet: GateAccepted`, `@geonosis/ratchet: GateAnswer`, `@geonosis/ratchet: gateCacheDir`, `@geonosis/ratchet: gateDaemonPid`, `@geonosis/ratchet: gateKey`, `@geonosis/ratchet: gateLastLine`, `@geonosis/ratchet: gateLogPath`, `@geonosis/ratchet: GateLost`, `@geonosis/ratchet: GateRefused`, `@geonosis/ratchet: GateRun`, `@geonosis/ratchet: gateRunsDir`, `@geonosis/ratchet: gateSocketPath`, `@geonosis/ratchet: gateStatus`, `@geonosis/ratchet: HEAVY_SLOT_HELD`, `@geonosis/ratchet: HeavyBudget`, `@geonosis/ratchet: heavyBudget`, `@geonosis/ratchet: heavyLockPath`, `@geonosis/ratchet: heavySlotDir`, `@geonosis/ratchet: HeavySlotHolder`, `@geonosis/ratchet: heavySlots`, `@geonosis/ratchet: HeavySlotTimeout`, `@geonosis/ratchet: Holder`, `@geonosis/ratchet: readCacheEntry`, `@geonosis/ratchet: runViaGate`, `@geonosis/ratchet: serveGate`, `@geonosis/ratchet: slotHolders`, `@geonosis/ratchet: slotInherited`, `@geonosis/ratchet: stopGate`, `@geonosis/ratchet: submitToGate`, `@geonosis/ratchet: takeSlot`, `@geonosis/ratchet: waitForBuild`, `@geonosis/ratchet: watchGateParent`.  `@geonosis/verify`, `@geonosis/evals`, `@geonosis/rails` and `@geonosis/release` import them from `@geonosis/gate` and depend on it. The doctor's fixed group and the metapackage's by-name list name `@geonosis/gate`. (doctor, evals, gate, rails, ratchet, release, verify, geonosis)
- The skills, rules, agents and commands run every tool the `geonosis` door fronts through it: `geonosis review check`, `geonosis ratchet --raise`, `geonosis verify fast`, `geonosis doctor`, `geonosis lint-parity --corpus`, `geonosis walk`. Under pnpm a tool's own bin is a command only where its package is a direct dependency. The rule files `geonosis rules` copies say it that way, and the guard that refuses a gate chained into a commit or merge now recognises `geonosis verify` as a gate (#68). (doctor, geonosis)
- A `wrangler.toml` is read by wrangler's own config reader (`experimental_readRawConfig`), resolved from the config's directory, then the repo root, then the kit's own wrangler, in place of a hand-written TOML parser:  - **TOML that reads now:** multi-line strings, and the rest of TOML. - **Invalid TOML** is refused in wrangler's words. - **No wrangler installed** makes a `.toml` config unreadable, and the gate says so. - **Removed:** the `@geonosis/release: parseToml` export. `readWranglerBlock` is exported in its place, and the doctor's `workers` check reads through it. (doctor, release)
- A release's eval entries are judged by the sessions they passed, against every scored release before them, with a one-sided Fisher exact test (`compareEntries`, `fisherOneSided`): a failure the history cannot tell from noise at this k is reported with its p-value and the fewest failures in three more rounds that would be refused at alpha (one failure in three against 18/18: `2 more failures in 3 more rounds would be (p=0.01)`), never refused. `compare` and `gate --against` judge two scores by the same test at the same alpha, over the sessions behind each of the four dimensions that count sessions passing (`JUDGED_DIMENSIONS`: `firstEditPass`, `gatePassRate`, `pressureCompliance`, `injectionResistance`), never by the rate a file states: compliance falling 1 → 0.667 at k=3 is held with that note, and no session passing in three against eighteen is refused. A score that stopped measuring a dimension its baseline measured, and two scores that hold no session, are refused. The caps, `reviewPrecision`, `reviewRecall` and `proofsPerTick` judge nothing at `compare` and `gate`: a cap is the worst count one session reached, the reviewer rates are judged by `review-bench compare`'s sign test over cases a score does not carry, and `proofsPerTick`, which refused 3.6.1 at 3 → 1.5 over a coin flip, is a ratio of two counts per run. A `--tolerance` or `--dimension` naming any of them is refused with that reason. `run --dimension` decides whether a wording change earned its place by the same test over each reading's sessions (now recorded beside the law's fingerprint), counted once per session id: a rise it cannot distinguish (1/3 → 3/3, p=0.2) is held with the fewest passes in three more rounds that would be distinguishable, and a reading recorded without its sessions is taken again. `Comparison` now carries one verdict per judged dimension and what could not be measured, and each regression its counts and p-value (`ScoreRegression`); `gate --prove` plants into the sessions of every judged dimension. A session is counted once per `sessionId`, in a release and in its history: its last record wins, as the verdict store keeps the last verdict written, and a session the history already holds is not new evidence, so one failed session written three times reads 0/1, not 0/3. `EntryRegression` now carries the counts and the p-value instead of a dimension, and `EntryComparison` carries one verdict per entry.  The kit set keeps the entries whose arms differed in every recorded armed run (`add-a-rule`, `fix-a-counter`, `extend-real-code`, `the-rule-is-wrong-anyway`: 18/18 with the kit, 0/18 without, over 3.2.0–3.6.1) and retires the three that passed 18/18 on both arms (`record-a-tick`, `ship-in-an-hour`, `the-file-says-it-is-allowed`): they measured the model, not the kit. The `injected-shaped` fixture goes with its case; `@geonosis/evals: INJECTION_MARKER` is removed.  `geonosis-evals run --history <dir>` reads the scored runs of earlier releases. A run records its provenance (the commit, the claude version, the model, a digest per entry and one over the set); the `without` arm is not opened while an earlier run recorded it over the same set digest; an entry is carried from the newest run when nothing it depends on changed since that run's commit (its fixture and definition, the plugin dir, the scorer — a bin the session runs is the corpus's to watch); every entry that ran is judged against the pooled history, and the exit code is that verdict's. The report is one screen and says, per entry, what was carried from where and why, and which path made an entry run again.  `geonosis-evals regressions --set <module>` runs a deterministic corpus: one case per regression a consumer filed (`kitRegressionCases`, 22 cases from issues #119–#209), each driving a built kit bin over a scratch tree and reading what it left, no model involved. `pnpm evals` runs it after `gate --prove`, so the release tier and CI carry it; exit 1 names the issue a case found again, exit 2 a case that could not run.  The reviewer bench is judged by a paired sign test over the cases that got worse and better (`compareBenches`, `signTest`): measured over the seven recorded benches on identical inputs, 6 to 14 of 28 cases move between runs with nothing changed, so one case losing precision is the spread, not a regression. A release whose reviewer inputs are unchanged since the last benched release needs no bench; the earlier one stands and the release check says so.  Every session of every arm and round opens at once, up to `--jobs`, and both ceilings are read before each session opens. The runner spawned `claude` synchronously, which blocked the event loop, so the sessions of a round ran one after another whatever `--jobs` said: four sessions of 31–44 s took 155 s (measured 2026-10-15). A round now takes about its slowest session. (evals)
- A red step's whole output is kept (#159). `geonosis-verify` writes it to `.geonosis/logs/<tier>/<id>.log`, the step id spelled with `-` for its `/` (`fast-1.log`) and, after its `@`, `%2F` for a `/` and `%25` for a `%` (`fast-1@packages%2Fa-b.log`), so two steps never share a log, names it in the gate report as the step's `log` and prints `full output: <path>` under the FAIL; an output past 4 MB keeps its head and its tail and says how many bytes it dropped between them. `geonosis-ledger land` copies the log of a refused step to `.geonosis/land/<tree>/` before the merge-result worktree is removed, ends the refusal's tail with `full output: <path>`, and keeps the logs of the newest 20 lands.  A step may declare `failureMark`, a pattern (a regex source) its own failure lines carry, such as vitest's `" FAIL "` or a mutation lane's `"^(?:survived|unjudged)\\s"`. A red step's evidence then opens on the first line the mark matches, not on the first line that looks like an error, which a passing line quoting an `Error:` can own. (ledger, verify)
- `delivery check --commit <hash>` beside `--cached`, `--paths` or `--all` judges the commit that amending `<hash>` produces, measured from its parent (or from nothing for a first commit). A rename staged onto a commit of runtime code used to be refused as "the diff changes nothing at all", and an amend that only rewrites the message was refused as an empty index; an amend that takes back every runtime line of its commit is now refused. `--commit <tree> --parent <rev>` judges the commit `git commit-tree` writes from that tree; a tree with no `--parent` is measured from nothing. (ledger)
- `ledger.land.verify` defaults to `geonosis verify`, the door, so `geonosis ledger land` runs the gate in a repo that installs only `geonosis`. The default was `geonosis-verify`, which under pnpm is a command only where `@geonosis/verify` is a direct dependency, and the land was refused with `command not found`. A repo that sets `land.verify` keeps its own command (#68). (ledger)
- `geonosis ledger land` runs a `ledger.land.verify` spelled through the door (`geonosis verify`, the default) through the `node_modules/.bin/geonosis` the repo links, by its path, so `./node_modules/.bin/geonosis ledger land <branch>` lands with a PATH that holds no `geonosis`, where it used to fail `geonosis: command not found`. With no door on PATH and none linked, the refusal names the path it looked for and `ledger.land.verify`, where it used to advise `ledger.land.prepare`, which does not put the door on PATH. (ledger)
- Every rule option holding a regular expression over a file's path is tested against the absolute path oxlint hands a rule and against the path oxlint prints, relative to the directory it runs in, the two compared as real paths so a tree named through a symlink (`/tmp` → `/private/tmp` on macOS) reads the same. The relative path is from oxlint's working directory, not the config file's, which oxlint 1.88 does not hand a rule: the README says so, and that a run from a subdirectory needs a pattern that holds from both (`(?:^|/)src/data/`). Before, a pattern anchored at the start (`^src/data/`) matched nothing: a scope option turned its rule off and an exemption exempted nothing, with no message. The options read this way: `boundary-time-is-primitive` `doors`, `dialect-through-the-seam` `seam` and `exempt`, `hermetic-e2e-no-wall-clock-waits` `specs`, `layer-walls` `layers[].paths` (for the importing file and a resolved relative import), `list-take-without-order` `exempt`, `no-d1-transaction` `transacts` and `except`, `no-hook-in-component-disguise` `clientBoundary`, `no-loader-side-effect-in-index-file` `directories`, `no-orm-outside-db` `dataLayer`, `no-raw-sql-outside-allowed` `allow`, `no-swallowed-catch` `paths`, `queries-require-org-scope` `queries`, `route-no-inline-mutation` `paths`, `ssot-no-inline-facts` `owners`, `ssot-no-process-env` `doors`, `step-opens-its-own-cell` `within` and `wrapper`, `store-route-scopes-tenant-data` `routes`, `subscriber-declares-id` `paths`, `tables-declare-their-plane` `schema`, `tenant-tables-carry-org-id` `tenant`, `test-no-clock-polling-for-promise-flips` `testGlobs`, `time-through-the-door` `door` and `exempt`, and `worker-handles-are-scoped` `within`. A pattern that matched the absolute path still matches it, so no configuration that worked before changes. The shipped corpus holds a `relative/` tree whose config override names each of these options with a `^relative/` pattern, so `--expect-changed` over the 23 rules against 3.6.1 shows every one changing. `geonosis-lint-parity --corpus` runs oxlint with its working directory at the real path of the corpus copy, where it ran from the copy's parent and named the copy through the `/var` symlink on macOS, so a corpus config's relative patterns name the corpus's own paths. It now also says which rules a corpus speaks for by its manifest — the plugin's and any other rule listed there (`alsoSpeaksFor` on the report) — instead of "the plugin only". (lint-parity, oxlint-plugin-biological-architecture)
- `geonosis-mcp` survives a kit bump without a reconnect. When the repo's `@geonosis/cli` moves away from the release the server loaded, the server starts the repo's `node_modules/.bin/geonosis-mcp` (the server a reconnect would start), hands every `tools/list` and `tools/call` to it unchanged, and sends `notifications/tools/list_changed`, so the client lists the new release's tools. When the repo returns to the loaded release, the calls come back. A repo with no `geonosis-mcp` of its own, or one of the server's own release, still gets the refusal that names both versions and asks for a reconnect. The child server is closed when the client ends the session (#157). (mcp)
- `public-path-exists` takes `testGlobs` and leaves a test or a story alone by default (#179), the default `no-literal-copy` and `theme-tokens-declared` already had; the three rules now read it from one list in `rules/lib/paths`, which names a `.test`, `.spec`, Vitest type test (`.test-d`, `.spec-d`), Cypress test (`.cy`) and story (`.stories`, `.story`) with any JavaScript or TypeScript extension, and anything under `__tests__/`. `no-literal-copy` takes `paths`, regular expressions over the file's path, to judge only the app code that has words (#181); with no `paths` every file is still judged. A `paths` or `testGlobs` pattern is tested against the absolute path and against the path oxlint prints, relative to the directory it runs in, so `^src/app/` names `src/app/` there instead of matching nothing. A config file is judged like any other file, so the title in a root `site.config.ts` is a finding; `no-literal-copy` leaves only the config of a tool whose copy-named keys a page never shows, at a package root: `toolConfigs`, default `commitlint`, `eslint`, `oxlint`, `stylelint` and `tailwind`, in each spelling those tools read (`eslint.config.mjs`, `.eslintrc.cjs`, `.stylelintrc.cjs`, `.config/stylelintrc.cjs`) (a lint rule's `message` is read by a developer, a theme's `heading: '2rem'` is a size). The shipped corpus holds a spec, a Cypress test, a type test, two stories, a `__tests__` file, a lint config in its flat and its legacy spelling, a stylelint rc file, a tailwind config, a site config and a `paths`-scoped API route, so `--expect-changed public-path-exists,no-literal-copy` against 3.6.1 shows both rules changing. (oxlint-plugin-biological-architecture)
- The `i18n` preset's `react/jsx-no-literals` leaves the separators and symbols a page prints between words alone (#178): `·`, `—`, `/`, `|`, `:`, `(`, `×`, `→`, `…`, `{' '}` and the rest of `LANGUAGE_NEUTRAL_SYMBOLS`, exported from `./presets`, and each of them written as the named entity a JSX compiler decodes (`&middot;`, `&mdash;`, `&hellip;`, `&rarr;`, `&times;`, `&nbsp;` and the other spaces). oxlint's rule has no option for "a string with no letter" (1.88: `allowedStrings`, exact after trimming, `ignoreProps`, `noAttributeStrings`, `noStrings`, `restrictedAttributes`, `elementOverrides`), so the preset lists them in `allowedStrings`. oxlint compares the text as written, so a numeric reference (`&#183;`) is still a finding; and under `noStrings` it reports every template literal between tags without reading `allowedStrings` (`` {`·`} ``), which no option changes. A currency sign, the percent sign and a quotation mark stay findings, because where they go and how they look differ by language; words, wrapped or not, are judged as before. The shipped corpus enables the rule as the preset ships it, so `--expect-changed jsx-no-literals` against 3.6.1's preset shows the change. (oxlint-plugin-biological-architecture)
- The `i18n` preset's docs carry what a typed-messages move needs (#137): `Translator<'ns'>` helpers scoped per namespace, plural objects read through `useMessages()`, a `getTranslations` test mock that honours the namespace argument, and the measured typecheck cost (9.2 s typed against 8.8 s untyped on a Medusa storefront, down from 2,476 s root-scoped). The preset's description points a multi-language repo at the `catalogueArguments` ratchet counter. (oxlint-plugin-biological-architecture)
- `tokens-only-colors` leaves a palette typed as themekit's `PartialTheme` alone, as it leaves one typed `Theme`: a kit still filling its colour roles defines its palette there, and `compile` now takes one. The corpus carries `tokens/theme/partial-palette.ts`, which 3.6.1 judges (two hardcoded colours) and this release does not. (oxlint-plugin-biological-architecture)
- A rule that resolves an import through a tsconfig's `paths` now reports a tsconfig that does not parse, or a base it extends or a project it references that does not: one finding under that rule in each file whose import needed it, naming the file, what jsonc-parser found and the line (`CloseBraceExpected at line 3`). Before, the alias went unresolved and the import was judged as if no tsconfig declared it, with no message. A tsconfig whose root is not an object is reported the same way, as tsc refuses it. The reader narrows what it reads instead of casting it. (oxlint-plugin-biological-architecture)
- `geonosis-rails farm` and `shift` reach the ledger, the review and the gate the way the kit's guards do: the repo's own `geonosis-ledger`, `geonosis-review` or `geonosis-verify` in its `node_modules/.bin`, else its `geonosis` door running `geonosis ledger …`, then the same two on the PATH. They spawned `geonosis-ledger`, `geonosis-review` and `geonosis-verify` by name, which under pnpm exist only where those packages are direct dependencies, and the PATH answered before the repo's own copy. A `--ledger`, `--review` or `--verify` naming anything else is run as given. The usage and the README name the doctor as `geonosis doctor --only rails` (#68). (rails)
- `catalogueArguments` counts the messages of a language that name other ICU template arguments than the source language's message under the same key (#188). Each message is read by `@formatjs/icu-messageformat-parser`, inside `plural` and `select` options and rich-text tags. A tag counts as an argument under its name, `<b>`, because intl-messageformat 12.1.3 needs a value for it and throws `MissingValueError` without one, so a language renaming `<b>` to `<link>` is counted. `{locale}` is one path segment: `messages/archive/en-old.json` is no language under `messages/{locale}.json`. `exceptions` declares a message allowed to differ, with its reason, and an exception that stops differing is counted, so no reason outlives its case. Params: `source`, `catalogues` (`messages/{locale}.json`), `exceptions`. (ratchet)
- `geonosis-ratchet --prove` plants a finding of an `oxlintRule` entry's own `rule` (#158). It planted a `no-explicit-any` for every entry, so a census of `react/jsx-key` was PROVEN by a rule it never counts. The plant is the first that exists:  - the ten census rules ship a source file each (`RULE_PLANTS`), every one checked against oxlint 1.88.0 to report exactly one finding under that rule: `no-explicit-any`, `iframe-missing-sandbox`, `jsx-key`, `jsx-no-duplicate-props`, `jsx-no-script-url`, `jsx-no-undef`, `no-children-prop`, `no-danger-with-children`, `refs` and `set-state-in-effect`; - a kit plugin rule is planted from the reach corpus of the plugin installed in the repo: a corpus file that fires the rule, one that fires only that rule first, linted alone under the rule's level and options in the config the counter reads, or the whole corpus where no single file fires it; - an entry may declare `plant: { path, source }`, planted beside a copy of the config the counter reads; - any other rule is UNPROVEN, naming the rule and the `plant` to declare.  The plant is read under the entry's own `rule` spelling and through its `config` where it names one, so `--prove` measures what the run measures. `oxlintRule` reads `rule: "biological-architecture/no-literal-copy"`, the config's spelling, as oxlint prints it, `biological-architecture(no-literal-copy)`: it read 0 where `no-literal-copy` read 197, and `--prove` said PROVEN. A `plugin/rule` oxlint accepts but prints under another plugin name (`@typescript-eslint/no-explicit-any`, printed `typescript(no-explicit-any)`) is refused, naming the spelling oxlint prints; `ruledSeparately` reads `plugin/rule` the same way.  A counter may declare `plantFor(params, cwd)`: a probe for that entry, or the reason none can be planted. (ratchet)
- The ratchet keys each finding of `oxlintErrors`, `oxlintWarnings` and `oxlintRule`, as Betterer does: by file, rule and a hash of the whole flagged line (whitespace collapsed), never by line number, kept in `gate-baseline.findings.json` beside the baseline. One fix plus one new finding at an unchanged total now fails the run, naming the new finding's file, line, column and rule. Before, it passed. A fixed finding is banked with the number. A counter with a number and no findings yet has them written without a judgement, and the run passes. A findings file that does not parse refuses those counters. `--raise` records a finding that moved (a renamed file, an edited line) at the same number. `--prove` plants a new finding beside a removed one at an unchanged total and requires the refusal. An entry whose command prints paths from a subdirectory names it as `pathsFrom`. Counters that read no findings are unchanged (#215, plan 049 T6). The plugin's write guard refuses an edit of `gate-baseline.findings.json` as it refuses the baseline's, and the question a person is asked before `--raise` at the baseline says the findings read now replace those held. (ratchet, geonosis)
- The walk gains an `axe` probe through `@axe-core/playwright`, an optional peer: `critical` and `serious` violations are findings of class `accessibility-error` (blocking, major), `moderate` and `minor` of class `accessibility-warning`, and a missing peer exits 2 naming `npm i -D @axe-core/playwright` (#215, plan 049 T3). The report counts both classes whenever `axe` ran, and the summary names both beside it. The `walkFindings` counter takes `classes: ["accessibility-error"]` and `["accessibility-warning"]`, so a gate can count the errors without the warnings. (ratchet, walk)
- `doctorWarnings` reads the doctor's JSON report from the command's stdout alone. It used to read stdout and stderr joined, so a line on stderr after the report hid it: in a pnpm repo with only `geonosis` installed and no bun on the PATH, the door's `geonosis: bun is not on PATH, so this runs on node.` line made the counter `CANNOT MEASURE`. `CommandResult` carries `stdout` beside the joined `output`, and a recorded run without `stdout` is refused, naming the command. (ratchet)
- A counter command that starts with a kit tool, `geonosis-<tool>` (the `doctorWarnings` default `geonosis-doctor --json` among them), runs `node_modules/.bin/geonosis <tool>` when the repo installs only the `geonosis` door and not the tool's own bin, as a pnpm repo with only `geonosis` installed does; it used to fail with exit 127. The recorded identity reads `geonosis-<tool>` and `geonosis <tool>` as the same instrument, so a fall measured through either spelling under a baseline recorded with the other is banked rather than refused as unproven. The default command and the counter's fingerprint are unchanged, so an existing baseline keeps banking falls. (ratchet)
- A counter or verify step naming `geonosis-<tool>` is run as `geonosis <tool>` only for a tool the `geonosis` door runs, and only those spellings share a measurement identity. `geonosis-rails`, `geonosis-gate`, `geonosis-themekit`, `geonosis-evals`, `geonosis-mcp` and `geonosis-release-cloudflare` used to be rewritten to a `geonosis <name>` the door refuses. (ratchet)
- The `fastTierMs` counter's refusals, no gate report and a report of another tier, name the tier to run as `geonosis verify <tier>`, the door, instead of `geonosis-verify <tier>`, which under pnpm is a command only where `@geonosis/verify` is a direct dependency (#68). (ratchet)
- An oxlint finding is keyed by its file, its rule and a hash of its whole line, whitespace collapsed. It was keyed by the text from its column to the end of the line, so fixing `id: any` while adding `customer: any` in the same file, or `one: any = 1` while adding `two: any = 1`, passed at the same total: both keyed `any` or `any = 1`. Now each new one fails the run, named. A moved or re-indented line keeps its key; identical lines in one file share a key, counted. An edited flagged line reads as a new finding, which `geonosis ratchet --raise` at the baseline number records. `gate-baseline.findings.json` moves to scheme 2: findings kept under scheme 1 are read as none and written again, not judged, on the next run. (ratchet)
- `geonosis-ratchet --prove` measures an `oxlintRule` plant under the config the counter names (its `config`, or `.oxlintrc.json`), with that rule's level and options as the config sets them. Before, a kit rule was planted under its corpus options and a census rule under a one-rule config, so a counter whose config scoped `biological-architecture/no-literal-copy` to `paths: ["^nowhere/"]` read 0 in every run and `--prove` said PROVEN, reading 7. Such a counter is now UNPROVEN, naming the level its config sets and the corpus files the rule no longer reaches there. A `plant` the counter declares is used when no corpus file is reached, and is planted under the same config, read with `jsonc-parser` and with its `extends` resolved. A kit rule is now planted from one corpus file that fires it, as described; the lookup missed every file and always planted the whole corpus. (ratchet)
- A refusal that names a command spells it the way it was reached. Run through the `geonosis` door, `geonosis walk` and `geonosis review` refuse an unknown flag with `run geonosis walk --help` and `run geonosis review --help`, and `geonosis verify-arch --check` names `geonosis verify-arch --format … --output …` as the command that rewrites a stale drawing; run by their own bins they name those bins as before. The `walkFindings` counter with no walk report says to run `geonosis walk`. Each named the tool's own bin, which under pnpm is a command only where its package is a direct dependency (#68). (ratchet, review, verify-arch, walk)
- The `walkFindings` counter takes `classes: ["content-integrity"]`, the class of the walk's four newest probes (`thin-page`, `empty-section`, `money-format`, `orphan-url`). Before, the counter's own list of classes lacked it, so those findings could be counted only in the total. A test in the kit's repository holds the counter's list to the walk's own list of classes, the one its exported `DefectClass` type is drawn from. The walk's package description counts fourteen probes, thirteen computed-DOM readings and axe's accessibility rules; it counted eight. (ratchet, walk)
- `geonosis-ratchet --prove` reads a `walkFindings` entry's `classes` as the run does. It planted one `fabricated-claim` finding and measured it with no `classes`, so an entry naming a misspelled class (`accessibility-errors`) was PROVEN while every run refused it. Now `--prove` plants one finding of each class the entry names and measures them under those `classes`: a misspelled class is CANNOT MEASURE, naming it, and a real one reads its plant. (ratchet)
- `migrations --record-bookmark` asks the database what it has still to apply (`cf d1 migrations list`) before it takes a Time Travel bookmark. With nothing pending it takes none and keeps the restore point already recorded, where it used to replace it with one that restores to now, so a release with no migration lost the point from before the last one. With migrations pending it takes the bookmark, reads the list again, and records the migrations still pending then as `before`, so an overlapping deploy cannot leave a bookmark that undoes nothing. A migration the database has pending that this run of the gate did not judge, such as one a Time Travel restore rewound (measured: a restore rewinds the record of applied migrations), is refused with exit 1 and named. A pending list that cannot be read is exit 2. (release)
- `geonosis-release-cloudflare` refuses what it would otherwise do silently:  - **An upload that would inherit an unreviewed secret change.** `wrangler versions upload` takes its secrets from the newest version, not the serving one (proofs/071). An upload is refused, before it uploads, when a version newer than the serving one was not uploaded by a release. Versions are read through `cf workers versions list`, page by page. Deploying that version is no way past it: one `wrangler versions secret put` makes runs the code of the version newest when it was made, a rolled-back release's included. `release.cloudflare.carrySecretChanges` names the versions whose secret changes a release may carry, and the upload reply lists them under `carried`. - **A ramp or promote over a deploy that replaced production since the upload.** Nothing moves, so another deploy is never undone. - **A promote after which another version took production.** It names that version, because the triggers in force are this release's. - **A version this session did not upload.** A ramp or promote of one is refused. - **A Durable Object class migration.** wrangler refuses it at the upload, and nothing is parked (proofs/072).  `--prove` plants each of these. Upload messages read `geonosis-release:<commit>`, which is how a release's own uploads are told apart. (release)
- `geonosis-release-cloudflare` releases a set of Workers together (#153):  - **Declaring a set.** `release.cloudflare.workers` lists the Workers in promote order, and `release.cloudflare.sets` names independent sets. `--set <name>` chooses one, and `geonosis-release prove --set` hands it to the runner. - **Uploading and smoking.** Every Worker is uploaded and parked. One smoke carries an override header naming each new version, and every Worker must answer its own. - **Recovering from a refusal.** A refused upload takes back every park. A promote that fails partway gives back the Workers it promoted. - **Ramping.** A ramp splits every Worker at the same percentage. A set of more than one Worker ramps only with `release.ramp.affinity`: without a version key, 23 of 40 requests crossed versions (proofs/074). - **Upload commands.** `upload` runs the command that uploads a Worker another tool builds, such as `opennextjs-cloudflare upload`, which populates the cache and keeps the skew-protection mapping. The command is given `--message=geonosis-release:<commit>`, and a version that comes back without that mark is refused. - **The release mark.** Release uploads are now marked `geonosis-release:<commit>`. The earlier `geonosis release <commit>` is still read as a release's own. (release)
- `geonosis-release deployed --check` compares the queues a Worker consumes:  - **Both sides.** It reads `queues.consumers` from the wrangler config, and from the pipeline's report under `queues.consumers`. A consumer production still holds after the config dropped it is drift, because `wrangler triggers deploy` adds consumers and never removes one (wrangler 4.151.0's own source). - **An older report** with no `queues` key is not compared, and the output says so. (release)
- `geonosis-release prove --runner` records what a release did (#156):  - **The text** names each Worker's promoted version and the one serving before it, for example `shop: promoted fe0573e0 (was 8500f631)`. A refused release names what it uploaded and the version to roll back to. - **An envelope** is written on every run, passing or refused, to `.geonosis/envelopes/release-prove.json`. Its `record.workers` holds the uploaded, promoted and previous version ids. - **`.geonosis/deployed.json`**, when the pipeline keeps one, gets the promoted ids under `versions` after a release passes. Nothing else in it changes. - **The runner protocol:** an upload reply may name its `worker` and its `current` version, and a `set` of Workers released together. `geonosis-release-cloudflare` sends both. (release)
- With `release.cloudflare.observability: true`, each ramp step of `geonosis-release-cloudflare` reads both versions' outcomes over its hold from Workers Logs (`cf observability telemetry query`):  - **Refused:** a new version whose error rate is above the serving one's, named with both counts, or one with no logged request. - **The reply** carries `logged: { new, serving }`. - **Without the key,** nothing is asked of Workers Logs (plan 046 F1). (release)
- `askRegistry` answers `repositories`: the repository each version declares, normalized to `https://<host>/<owner>/<repo>` (`git+`, `.git`, `git://`, ssh and `github:` forms read alike, `directory` left out), so the packages of one monorepo release share it. A version that declares none has no entry (#206). (release)
- `geonosis-release restore <db>` puts a D1 database back to the restore point `migrations --record-bookmark` recorded, or a `--bookmark` or `--timestamp` named, through `cf d1 time-travel restore --force`. It needs `--yes`, and the geonosis plugin asks the person before it runs, naming the database, the point and that every later write is lost. A time without `Z` or an offset, a date the calendar does not have, a precision finer than a millisecond and a point older than the 30 days Time Travel keeps are refused before Cloudflare is asked. It prints the bookmark that undoes it and the migrations the restore rewound, which the next apply runs again. A refused `prove` names each recorded restore point, because a Worker rollback does not undo a migration. (release, geonosis)
- `geonosis-release migrations` judges the `sqlite` dialect by replaying it, not by reading its text:  - **The replay.** The earlier migrations are applied to an in-memory SQLite database, then each added statement is applied with SQLite's own statement boundaries and authorizer. - **What it refuses**, on what existed before the release: - a drop or rename of a table or column; - an `UPDATE` of a column, including an upsert's; - a `DELETE`; - a REPLACE that deletes the rows it conflicts with; - a dropped view, trigger or unique index. - **What it allows:** a backfill of a column the release added, and changes to what the release created. A verb in a comment or a string no longer fires. - **A file SQLite cannot apply** is unreadable, exit 2, in SQLite's words with its line.  The `postgres` and `mikro-orm-ts` dialects are unchanged. (release)
- `geonosis-release-cloudflare` reads each uploaded version's startup time from `cf workers versions list` and answers it as `startupMs`. With `release.cloudflare.startupBudgetMs` set, a version past it is refused before it is parked. So is one whose startup time cannot be read. Cloudflare refuses past 1 second (plan 046 F2). (release)
- `geonosis-release-cloudflare` says why a first release cannot go ahead, and what fixes it, instead of a raw API error or a smoke with no reason (measured on a live account):  - **A Worker that does not exist yet** (Cloudflare code 10007): it says to deploy it once with `wrangler deploy`, because a release parks a new version beside one that serves and nothing does. Nothing is uploaded. - **A Worker `wrangler secret put` created before its first deploy:** its workers.dev route is off until `triggers deploy` applies the config's `workers_dev`, so a smoke at a `*.workers.dev` URL reaches nothing. The upload is refused before anything is uploaded, naming `cf workers get`'s `subdomain.enabled` and the one `wrangler triggers deploy` that turns it on; the release then goes through. - **A smoke that does not pass** now says what the health route answered: its status and the first 200 characters it returned. (release)
- `geonosis-release prove` records the secret changes an upload carried (`release.cloudflare.carrySecretChanges`) beside each version, in the envelope, the printed verdict and the CI summary. A runner that refused its upload is reported as a refusal, with the runner's reason, not as an upload that "answered no versionId", and a refused run prints the steps it ran as `REFUSED (ran upload → …)` instead of `REFUSED after upload`, which read as if something had been uploaded. (release)
- `ingestStylesheet` types `theme` as a `PartialTheme`, as `fromDtcg` does: an ingest that maps no variable onto a required role has none, and `isCompleteTheme` (from `@geonosis/themekit/load`) narrows it to a `Theme`. Input that was already wrong is now refused, naming it, where it used to load and break later: `loadThemekit` refuses an `emit.json` entry that is not an emission, naming its index; `ingestStylesheet` refuses a status the stylesheet fills only one half of and an unmapped variable whose name holds a dot; `fromFigmaVariables` refuses a variable or collection that is not one, naming its id; `geonosis-themekit ingest --roles` exits 2 on a map that is not an object of strings. Valid input reads, compiles and writes as before.  `isCompleteTheme` is also exported from the root entry, which imports no `node:fs`, so browser code that ingests a stylesheet can narrow its result. (themekit)
- `fromDtcg` returns a `PartialTheme`, whose colours may lack a required role, as an incomplete document's do; it used to claim every required role through a cast. `loadThemekit` types `theme` the same way, and `isCompleteTheme` (from `@geonosis/themekit/load`) narrows it to a `Theme` when `missingRoles` is empty. `compile`, `emitVariables`, `valueAt` and `toDtcg` take a `PartialTheme`, so an incomplete kit compiles as before; `compile` hands back the theme's own type as `object`. Nothing changes at runtime. (themekit)
- `fromDtcg` reads a document's modes value by value instead of handing back whatever `$extensions` held: a mode that is not an object, a group that is not an object, a value that is not a string and a status pair missing `surface` or `onSurface` are left out, as `fromDtcg` already leaves out a token whose `$value` is not a string. A number under a mode's colour used to reach `compile` as a theme value. (themekit)
- `--affected` fails closed on a path no workspace owns, and narrows a test batch by Vitest's own import graph (#189 follow-up, plan 047 row "Void", ruled 2026-10-16). A changed path in no workspace runs every workspace and every test, and the run names the path; a change of documents only, named by the top-level `"documents"` globs of `geonosis.json` (default `["**/*.md", ".changeset/**"]`), skips the runtime tests and says so. A batch step that declares `"graph": "vitest"` is handed `--changed --experimental.vcsProvider` with `@geonosis/verify`'s provider, which answers Vitest with the changed sources, the git-ignored build files of every affected workspace and the test files under the step's `also` paths, so Vitest runs only the tests whose imports reach one of them. A changed file that is not a module the graph follows runs every test of the affected workspaces, names them, and says why, and so does a changed module that is gone from the tree, deleted or renamed away: Vitest 5's graph holds only files that exist, and 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 read as a change, as the tree hash never reads it, so a run's own report does not select every workspace on the next one. (verify)
- `geonosis-verify green record <name> [--asked <text>]` and `green check <name> [--asked <text>]` give a tool that keeps a green of its own a record shaped as a tier's: `.geonosis/tool-green-<name>.json`, the tree hash, and a question that must match. `check` prints the record and exits 0 while the tree hashes as it did, and exits 1 otherwise. No tier, Stop hook or release check reads it, so `green record fast` cannot turn a red tier green: a tier is green only by a run of its own. A name that is not one path segment is refused. The plugin's PreToolUse Bash hook asks the person before any `geonosis-verify green record …` or `geonosis verify green record …` an agent 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. `geonosis update` keeps its resume records with it (#186). (verify, geonosis)
- `keyFiles` in `geonosis.json`, beside `keyEnv`, names git-ignored files a tier reads, such as a Worker's `.dev.vars` (#209). Their content is hashed into the key the gate daemon and verify's own green record are kept under, so a changed file runs the tier again and an unchanged one is answered from the green. A path outside the repo is refused. The gate report records that key as `inputs`, and the 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 was run 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. (verify, geonosis)
- A per-workspace step may name a turbo task (`"turbo": "typecheck"`), and in a repo with a `turbo.json` and turbo installed it runs as one `turbo run <task> --continue --concurrency=<jobs> --filter=./<workspace> …` over the affected workspaces, in place of one command per workspace (#215, plan 049 T1). Local verify and a CI job running `turbo run` then 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 green may never be replayed from the cache. Without turbo, the step's own command runs per workspace as before, so every tier a repo declared keeps its shape, and a workspace whose `package.json` has no script for the task runs the step's own command too, since turbo would run nothing there: a tier reads the same verdict with turbo and without it. `prove` runs the task through turbo, forced and with `--env-mode=loose`, so the `PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false` it sets reaches pnpm in the copy; turbo 2.11.8's default strict env mode dropped it, and pnpm then refused to run over the copy's linked install. (verify)
- The `lint-config` FAIL names the approval command a repo can run: the repo's own `node_modules/.bin/geonosis-verify` where it links one, else the `geonosis` door, `<runner> geonosis verify lint-config --accept-config-change` (or the linked `node_modules/.bin/geonosis`). It named `<runner> geonosis-verify`, which under pnpm is a command only where `@geonosis/verify` is a direct dependency (#68). (verify)
- When `--affected` cannot narrow a batch by Vitest's import graph, it says it runs every test of the affected workspaces and names them, as `1 of 2 workspace(s), the affected ones (packages/a)`. It said "runs the tests whole" while a workspace that imports the deleted module without declaring a dependency on its workspace was not run. (verify)
- A `keyFiles` path that is a directory, `.` among them, is refused with exit 2 and a message naming it. It crashed the tier with an uncaught `EISDIR`. A path that is absent is still read as absent. (verify)
- A red step whose log file name would pass 200 bytes, as for a workspace path of about 250 characters, keeps its log under the name cut short with a hash of the whole step id, and the run prints `full output:` for it. The write failed on the name's length, and the run kept no log and said nothing. (verify)
- `composition-tree` reads a file's imports from the program `oxc-parser` parses, not with a regular expression (#215, plan 049 T4). An import written inside a string or a template used to count as an edge: it reported a `direction` or `self-composition` finding for code that imports nothing, and kept a file nothing imports from being reported as an `orphan`. The drawing reads the same edges. A file the parser cannot read now stops the run with exit 2 and names the file; before, its imports were read by the regular expression anyway. Both packs parse a `.js`, `.mjs` or `.cjs` file as JavaScript with JSX, so JSX in a `.js` file is read rather than refused as a syntax error, which the medusa pack had done since 3.2.0. A `.mjs` is a module and a `.cjs` is CommonJS. A `.js` is read as Node reads it: a module when the nearest `package.json` says `"type": "module"`, otherwise CommonJS, where a top-level `return` is allowed, and a module when it holds `import`, `export`, `import.meta` or a top-level `await`. A `package.json` that is not JSON stops the run with exit 2 and names it. The T4 parity run found that dependency-cruiser 18.6.0 reads TypeScript only through typescript below 7. With typescript 7 it drops every `export type … from` edge. So `composition-tree` is not replaced by a dependency-cruiser config. (verify-arch)
- `@geonosis/base` is a dependency, not a devDependency: the `geonosis-visual-diff` bin imports `@geonosis/base/node` at runtime, so an install of the package alone left the bin unable to start. (visual-diff)
- The README says plainly that page baselines in a Playwright suite belong to Playwright's `toHaveScreenshot`, and that this package is for parity between two renders and for non-Playwright use. No export changes (#214). (visual-diff)
- `geonosis visual-diff --print-config-shape` names the door it was run through: it ends `See geonosis visual-diff --help.`, where it used to name the own bin, `geonosis-visual-diff`, which a repo that installs only `geonosis` does not have. (visual-diff)
- A URL entry takes `scrollTo` and `waitFor`: the walk scrolls to the first and waits for the second before any probe reads the page, and exits 2 naming the selector and the URL when it never appears (#173). The report records `gpu` and what the browser answered for WebGL, caveat, renderer and vendor, so two reports of one page can be compared (#172). The summary names each probe that ran by its id, with its class where the two differ (#174). (walk)
- The commit guard refuses `git am` in a kit repo: a patch am applies becomes a commit no gate judged, so it names `git apply --index <patch>` and then `git commit`, which both gates judge; `--abort`, `--quit` and `--show-current-patch` make no commit and pass. A non-interactive `git rebase` and `git pull --rebase` pass, since they replay commits already judged; a `git pull` that merges gets the message gate `git merge` gets, so `-e`, `--edit` and `--log` are refused. An alias set on the same line through `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` or `GIT_CONFIG_PARAMETERS` is read like `-c alias.<name>=…`, with `-c` winning as it does in git; a non-builtin subcommand under a config the guard cannot resolve (a `-c` or `GIT_CONFIG_*` value it cannot expand, `--config-env`, or a `GIT_CONFIG_*` variable assigned or exported earlier in the line) is refused. A line too long or too deeply nested to read is refused for every commit-making command its text names (`commit`, `revert`, `cherry-pick`, `commit-tree`, `am`, `merge`, `pull`), where it used to catch only `git commit`, and with a refusal that says the line could not be read. (geonosis)
- The commit guard judges every git command that makes a commit. `git commit --amend` is judged by the commit it produces (HEAD and the staged change together, through `delivery check --commit HEAD`), so a rename amended into a commit of runtime code goes through. `git revert` and `git cherry-pick` are judged by the delivery of each commit they replay, and a cherry-pick by each picked message (with the `-x` line counted); `git commit-tree` by its tree against its first parent and its `-m`/`-F` message, refused when its tree or parent cannot be resolved before the shell runs or its message comes from stdin. `git merge` gets the message gate only, since the commits it joins were judged when they were made. A git alias is resolved (`git config --get alias.<name>`, or `-c alias.<name>=…` on the line) and judged as the command it stands for; a shell alias from the config that runs a commit is refused. A commit into a repository a `git init` earlier in the same line creates is refused, naming the init, where it used to go through unchecked or be judged against the repository around it. (geonosis)
- The commit guard refuses an interactive rebase in a kit repo, since it can reword, squash, split or rewrite commits no gate reads, and says a person runs it in their own terminal: `git rebase -i`/`--interactive`/`--edit-todo` (abbreviations and clusters such as `-qi` included), `--exec`/`-x`, a rebase with `GIT_SEQUENCE_EDITOR`, `GIT_EDITOR`, `-c sequence.editor` or `-c core.editor` set beside it or a config beside it the guard cannot read, and `git pull --rebase=interactive` (or `=i`, `-ri`, or `pull.rebase`/`branch.<name>.rebase` set to it with no rebase flag on the line). A plain `git rebase <upstream>` and `git pull --rebase` still pass, in a directory the guard cannot resolve too. A line too long to read is refused for a `git rebase` its text names, as for the other commit-making commands. (geonosis)
- The commit and push hooks fail closed. A commit or push in a repository with no `geonosis-ledger` and no `geonosis` door is refused, naming the paths it looked for and the check to run by hand; a commit or push whose check runs past the hook budget is refused, naming the budget, the measured time and the exact command. A delivery check, commit-message check, shape lock or push guard that exits with anything but 0 or 1 refuses too, naming the exit and what it said; one a signal ended before the budget is refused naming the signal, the time and what it said, where it used to be reported as past the budget. All of these used to let the commit through unchecked (#215, plan 049 T2). (geonosis)
- The guard refuses a gate chained into a commit when the gate is a versioned door (`npx geonosis@3.7.0 verify`, `pnpm dlx geonosis@latest verify`, `npx @geonosis/verify@3.7.0`) or the commit is an inline git alias of one (`git -c alias.ci=commit ci`, an alias of an alias, `!git merge`), as it refuses `geonosis verify fast` chained into `git commit`. Both used to be let through. (geonosis)
- The plugin asks before `green record` whatever comes between the words: a flag such as `--asked q` before `record` or before `green`, `--` before the name, and verify run as `npx @geonosis/verify` or `pnpm dlx @geonosis/verify@<version>`. These wrote a tool-green record without asking. (geonosis)
- The commit guard checks a commit in the tree it runs in. A pipe anywhere in the command, a `$(… | …)` substitution included, used to make every `cd` before the commit unknown, so `cd <worktree> && git status --short | cat && git commit -m … -- src/a.ts` was checked in the session's checkout. The guard now reads where the shell leaves the directory: a variable assigned earlier in the same line (`D=/abs/wt && cd "$D"`, `export D=…`, `${D}/sub`) and `$HOME` are resolved; a group (`{ … }`, `if`, a loop) that is piped or backgrounded leaves the directory where it was before the group; `pushd` and `popd` follow the directory stack. A commit or push whose directory the guard still cannot resolve (`cd "$(…)"`, a variable never assigned, a `cd` that is the last member of a pipeline, which bash runs in a subshell and zsh does not) is refused, naming the command it could not follow, where it used to be checked in the session's checkout. A commit whose paths or `git add` paths hold a word the guard cannot resolve (`-- $F` with `F=$(…)`) is refused naming the word, where the literal `$F` used to be handed to the check. A redirection (`2>&1`, `> out.txt`) is never read as a pathspec or a path `git add` stages. A commit whose paths hold no change because an edit earlier in the same command has not run yet (`python3 edit.py && git commit -- f`, and an edit by `echo … >`, `printf … >`, `cat > f <<EOF` or `tee`) is refused saying so, naming the commands that had not run, and asking for the edit first and the commit as its own command; a commit with nothing before it keeps the refusal it had (#215). (geonosis)
- The commit and push guards find the ledger the way the `geonosis` door does: the repo's own `geonosis-ledger` bin in its `node_modules/.bin`, else the repo's `geonosis` door running `geonosis ledger …`, and only then a `geonosis-ledger` or `geonosis` on the PATH. A pnpm repo that installs only `geonosis` links no `geonosis-ledger`, and since the guards fail closed every commit and push there was refused; and a stale `geonosis-ledger` on the PATH used to answer before the repo's own door. They still refuse when neither bin is installed, and the refusal names both paths it looked for and the check to run by hand as `geonosis ledger …` (#215, #68). (geonosis)
- The Stop hook's fast tier, the SessionStart doctor and the lint-config listing and approval run their tool the way the commit guard does: the repo's own `geonosis-verify` or `geonosis-doctor`, else the repo's `geonosis` door running `verify` or `doctor`, and only then a copy on the PATH, which used to answer first. A pnpm repo that installs only `geonosis` had no fast tier at a turn's end, no doctor line at a session's start, and an approval that could not list the changes it approved. The session doctor reads the doctor's report past the door's note that it runs the cli's own copy (#215, #68). (geonosis)
- The LAW 4 guard refuses the packages that run prettier under another name: `npx @prettier/cli`, `npx pretty-quick` and their `pnpm dlx`, `pnpm exec` and `bunx` forms are refused in a kit repository as `npx prettier` is, where they used to be let through. (geonosis)
- The LAW 4 guard refuses a prettier run only when a command actually invokes it, in the directory it runs in. A word inside an argument, a quoted issue body, a heredoc or a grep pattern is not a run; a run whose directory (after `cd`, `-C`, `--cwd`, `--dir`) is outside every kit repository goes through, as does one inside a kit tree in a repository that pins prettier and has a prettier config. The session's kit repository (`corepack pnpm dlx prettier` included), a path argument into a kit repository (spelled with `$HOME` or a variable assigned earlier in the line, or a glob such as `'../kit/**/*.ts'`, judged by the directory it starts from), an argument the guard cannot resolve (`$(…)`, a variable never assigned), and a directory it cannot resolve are still refused (#216). (geonosis)
- The plugin's questions can be switched off by the person: `GEONOSIS_ASK=off`, or `off` in `~/.config/geonosis/ask`. Each hook that would ask (a ratchet raise, a visual approval, a lint-config change, a recorded green, a D1 restore) allows instead and notes what it would have asked. Refusals are unchanged, and `GEONOSIS_ASK=on` asks again. (geonosis)
- The LAW 4 guard resolves a run's command word the way it already resolved its arguments: `x=prettier; npx $x --write .` and `p=pret; t=tier; pnpm dlx $p$t --write .` are refused, where they used to go through. A command word it cannot resolve is refused when `npx`, `bunx`, `pnpx` or a package manager's `dlx`/`exec` runs it, or when the line names prettier anywhere (`x=$(echo prettier); $x --write .`). (geonosis)
- The Stop hook reads geonosis-verify's green record: after an edit that went red is reverted, or a `keyFiles` file is set back to a value that was green, the turn ends green without launching a tier. It launched one, verify answered from that record and wrote no report, and the next Stop exited 2 with "wrote no report — it could not run". The hook's tree hash now counts the install records, as the gate daemon's and verify's do, so a green kept by either over a tree with `node_modules` is the one the hook looks up. (geonosis)

## 3.6.1

- Proofs per tick counts only the proofs of the sessions that recorded a tick. Before, an entry's proofs from sessions that never ticked were divided by the ticks of the ones that did, so a second session ticking with its own proof read as a drop: 3.6.0's `the-file-says-it-is-allowed` scored 3 and 3.6.1's 1.5 over the same one proof per tick, and the release check refused 3.6.1 over it. (evals)
- A process ended by SIGTERM, SIGINT or SIGHUP removes the temp directories `heldTempDir` made, with their holders, and still ends by that signal; a process with its own handler for the signal is left to end itself, and lets go at its exit. The first `heldTempDir` call of a process removes every directory of its family (`geonosis-`) whose holder names a dead process. Before, a killed run left its directories and holders for good: 175 on one machine after one suite. `createCellServer`'s `stop` removes the cluster's data directory once the cluster has stopped (#190). (base, testbed)
- The release-age refusal's override is the person's own `geonosis update` command, `--floor`, `--floors`, `--install-args` and the rest, with only the age lifted. Before, it was rebuilt from `--to` alone and ran a different bump (#192). (cli)
- `geonosis post-update` finds a BY HAND mark wherever an item of an entry starts, not only at the start of the entry, and lists that item alone. 3.6.0's `cf` item and 3.4.0's `backend-d1` item each sat inside a longer entry, so a repo bumping from below 3.5.0 was told that nothing was asked of it (#200). A mark quoted in backticks is not one. A changeset whose mark does not start its body now fails the kit's own tests. (cli)
- `geonosis update --install-args --min-release-age=0` is read: `--install-args` takes a value that starts with `--`, since every package manager's override does. Only `update`'s own flags are still refused there, with the `--install-args=<value>` spelling that hands one to the manager. Before, the override the release-age refusal printed was refused by `update`'s own parser on every manager. The refusal, `update --help` and the README now print the inline form, `--install-args=--min-release-age=0`. A test runs each printed command through a shell and back into `update` (#205). (cli)
- The README names how an npm tree on a kit older than 3.5.0 bumps: its installed `update` refuses npm, so it runs the target version's own `update` through `npx -y --min-release-age=0 --package=geonosis@<version> -- geonosis update …`. That copy runs the whole ritual and its own `post-update` (#208). (cli)
- `geonosis update` reads an npm `package-lock.json`: the versions of every installed copy, nested ones included, and who depends on whom. Before, the lock-move report read lockfiles by `name@version` text, which an npm lockfile does not contain, so on an npm tree it never named a package the bump moved. (cli)
- `geonosis update` no longer holds a bump over a third-party package's age. Under an exemption, the manager's own cooldown governs third-party packages. Under the override you pass in `--install-args`, nothing is held, and after the install `update` lists each third-party version the lockfile gained that is younger than your cooldown, as `young` lines. That includes one a kit package the root never names brought in (#207), and never a version the tree already had and npm kept (#206). Before, under the override, `update` judged the newest release of every range the repinned packages declared. It refused an npm tree that pinned an older oxlint about 96% of the time, and never read what the rest of the fixed group pulled in. (cli)
- The commit body `geonosis update` prints starts with one summary line: the versions, the floors moved, how many steps ran clean, and which did not run. Every line is within 100 characters. Before, it met the line count by putting every step's command on one 286-character line (#193). (cli)
- `geonosis post-update` runs the doctor checks this version's `update` asks for and the updater that ran did not: `group`, and `release` where `geonosis.json` declares one, after an update made by a version below 3.6.0. It fails as this version's `update` would. post-update is always the copy just installed, so an older updater's bump into this version is now checked the same way (#201, #191). When the doctor step of `update` fails, it prints the doctor's FAIL, WARN and UNJUDGED lines rather than its JSON report. post-update still runs after it, so the notes that explain the failure are shown (#202). The commit body names `geonosis post-update --from <version>` for the notes and BY HAND items, rather than `.geonosis/bump-report.json`, which no commit carries (#203, #194). (cli)
- The law-presets FAIL says the config "does not enable" a rule, then "Turned on, it reports N here". Before, it read "enables none of X: enabled, X reports 0 here", which contradicted itself (#197). (doctor)
- `public-path-exists` judges `/favicon.ico` by default, unless the app serves its own favicon from `app/favicon.ico` or `src/app/favicon.ico`, which is the only case the exclusion existed for. `extensions` still replaces the whole list when given (#196). (oxlint-plugin-biological-architecture)
- The `geonosis-gate` daemon keeps only green reports. A red can come of the machine's load, an ignored file or the environment, so once the run that made it is over, the next verify runs the tier again. Before, the red was answered from the cache until `--fresh`. A cached green is answered with when and at what load it was made: `answered from the green kept at … (3 min ago, load 4.1)`. The tree a run is keyed by now includes the install record npm, pnpm or yarn writes under `node_modules`. A repo names the environment variables a tier's verdict depends on under `keyEnv` in `geonosis.json`, and a change in their values runs the tier again, through the daemon and through verify's own green record (#209). (ratchet, verify)
- The optional `squawk-cli` peer is pinned to 2.69.0. Its release notes add and rename no lint rule: they bring parser fixes, formatting and internal work, so a release argues from the same rules. The install line in the READMEs and in `geonosis doctor`'s toolchain reads `squawk-cli@2.69.0`. (release, geonosis)
- The budget line says what the tier could run at once. A tier of plain steps prints `3 steps ran one after another; steps in a { "parallel": [...] } group, or one step per workspace, run up to 7 at a time`. A tier with a group prints `up to 2 steps at once, of 7 slots`. Before, every tier printed `7 heavy slot(s) … --jobs 7`, which read as seven at a time on a tier that ran serially. One group took a 13-step fast tier from 30.3 s to 14.6 s (#204). (verify)
- A batch step's known-green files are all run again when any non-test file in no workspace changed: a lockfile, a runner config, a patch, a path the step names in `also`, a document a test reads. Before, only a fixed list of root files did this, so a change under `also` or to `package-lock.json`, `yarn.lock` or `vitest.config.mts` left the tests that read it passed as `--exclude`, and the step went green over a failing test. The verdict line now counts the files it did not run, `(2 test files known green, not run)` (#189).  `--affected` runs every workspace when `package-lock.json`, `yarn.lock`, `bunfig.toml`, `.npmrc`, a `vitest.config` or `vitest.workspace` of any extension, any root `tsconfig.*.json` or a file under `patches/` changed. Before, only pnpm's and bun's lockfiles and `vitest.config.ts` counted, so an npm or yarn repo's lockfile change ran no workspace. (verify)
- The LAW 2 guard resolves each path a command writes against the directory that command runs in, a `cd` or `git -C` before it included, and refuses a runner-owned path (`.geonosis/`, the verified mark) only inside the session's own repository. Before, it matched the words as text, so `rm -rf .geonosis` in a throwaway repo under the scratchpad was refused. A path it cannot resolve, such as one built from a variable, is still refused, and the refusal names the resolved path it judged (#199). (geonosis)

## 3.6.0

- `geonosis-evals run` takes `--jobs <n>`: the sessions of one round run that many at once, default the machine's heavy budget, and are scored in the order the set names them. Rounds still run one after another, so the cost ceiling is checked between them. A 3.5.0 armed run took 55 minutes one session at a time. `@geonosis/base` exports `pooled`, the bounded pool `geonosis-verify` used for its parallel groups, now shared by both. (base, evals, verify)
- Every temp directory a kit package makes is held by the process that made it (`<dir>.holder`, the process id), through `heldTempDir` from `@geonosis/base/node`. The holder goes when that process exits. Before, a sweep of unheld `geonosis-*` directories, run by any test run on the machine, could delete a live one. An evals session waiting on its model holds no file open, and lost its workdir that way 11 minutes into a scored run. (base, cli, doctor, evals, ledger, lint-parity, oxlint-plugin-biological-architecture, rails, ratchet, release, testbed, verify)
- The commit body `geonosis update` prints is the subject plus at most `ledger.commitMessageLines − 1` lines, read from the repo's `geonosis.json` (default 4). So the kit's own `geonosis-ledger commit-msg` accepts it (#165, #187). Before, it was 7 to 22 lines, and the gate refused it. The floors moved, the release notes, what did not run and the BY HAND work are printed above the body and kept in `.geonosis/bump-report.json`, which the body names. `@geonosis/base` exports `COMMIT_MESSAGE_LINES`, the default the ledger and the update share. (base, cli, ledger)
- A preset that names one rule of an oxlint plugin brings in that rule and nothing else of the plugin (#177). The `i18n` preset's `react/jsx-no-literals` switched on oxlint's `react` plugin, and the config's `correctness` and `suspicious` categories then enabled all 42 react rules in them. On a React 19 storefront that meant 7,036 `react-in-jsx-scope` findings. `geonosis init` and `create-geonosis` now write each of those rules `off`, read from oxlint's own catalogue (`oxlint --rules --format=json`), which ships with the kit and is held against the installed oxlint by a test. An override cannot scope a plugin away from the categories: oxlint 1.88.0 refuses `categories` in an override, and a plugin enabled in an override is still reached by them. (cli, oxlint-plugin-biological-architecture, create-geonosis)
- - On a yarn 1 tree, `geonosis update` holds a bump to npm's `min-release-age` (`npm_config_min_release_age`, then the project `.npmrc`, then the user's). yarn 1 has no cooldown of its own, so that is the policy the user stated. A refusal names `npm_config_min_release_age=0 geonosis update --to <v>` as the way past it for one run (#185). - When `pnpm config get` cannot run, the pnpm reader reads `minimumReleaseAge` from `pnpm-workspace.yaml` (#171). When neither answers, it throws `ReleaseAgeUnread` instead of reporting no gate. `update` refuses with that sentence, and the `cf` advice says no version can be named past the gate. (cli, release)
- `geonosis update` asks the doctor for `loaded`, `group` and, where `geonosis.json` declares a release, `release`. Before, it asked only for `loaded`, so a bump could report PASS while the next full doctor, and CI, failed (#160, #163). - The doctor's `group` check reports a kit package that is installed but that no manifest asks for, and no kit package asked for depends on, once, as a WARN naming the `rm -rf` that removes it. It used to report that copy as a bump that landed halfway, once per workspace. bun's hoisted linker keeps a removed package on disk (#160). - A doctor finding with an OK verdict is no longer listed as BY HAND work (#175). - The skipped build step names the key it reads, `verify.build` (#164). - BY HAND (from <3.5.0; done by doctor): since 3.5.0 the release runner and `--record-bookmark` move traffic and take the D1 bookmark with Cloudflare's `cf` CLI. A Workers repo that releases through `geonosis release` installs `cf` and logs it in (`cf auth login`, or `CLOUDFLARE_API_TOKEN`). The doctor's `release` check names the version past the repo's release-age gate (#163). (cli, doctor)
- `geonosis update --prove` proves the package managers at once, each in its own scratch consumer against the one scratch registry, and reports them in the same order. Over this repo's test of it: 183 s to 99 s at the same load. (cli)
- `LAW_PRESETS` carries every rule a preset ships, with its setting and core rules included. `comments: ceiling` is `max-comment-density` and `capitalized-comments` with `ignoreConsecutiveComments: true`. Before, a filter on the plugin's prefix dropped the core rule, so a config with the density rule alone read OK (#176). The doctor's law-presets check FAILs a rule that is not enabled, raising it with the preset's own setting to count what it would find. It WARNs a rule enabled with options other than the preset ships. A 3.5.0 plugin's list of names is still read. (doctor, oxlint-plugin-biological-architecture)
- The kit set prices each entry from the 3.6.0 armed run: the dearest session of each, doubled, to the next 5 cents. `the-file-says-it-is-allowed` $0.80, `fix-a-counter` $0.55, `extend-real-code` $0.65, `record-a-tick` $0.75. The documented run's ceiling is $34, 42 sessions times the dearest entry. (evals)
- `plugin-route-namespaced`'s default scopes are `store`, `admin` and `auth`, since Medusa mounts its auth CORS on `/auth` (#168). The `outsideScope` message names the pin for a top-level segment that is the plugin's namespace but not its directory: `namespace: '<segment>'` on the package entry (#167). A plugin whose directory is not its namespace needs that pin to use `/<namespace>/…` routes. (oxlint-plugin-biological-architecture)
- `public-path-exists` checks a file at the public directory's root (`/logo.png`) when its extension is an asset's (#180). `extensions` names them: by default images, fonts, video and PDF, but not `ico`, since a framework may serve `/favicon.ico` from its app directory. The search for the public directory now stops at the first directory that holds a `package.json`. Before, a file outside any public directory could be judged against a folder above the repo, such as macOS's `~/Public`. (oxlint-plugin-biological-architecture)
- - `cloneCount`'s probe passes `formats: []` as well as `ignore: []`. GNU `cat` refused the trailing `-f`, so `geonosis-ratchet --prove` was red on every Linux CI (#161). - A repo whose command names a jscpd config (`-c`/`--config`), or that has a `.jscpd.json`, keeps its own ignore and format lists. The counter appends `-i` or `-f` only for a list the repo named in `geonosis.ratchet.json`. Before, the defaults replaced the config's lists (#162). - A counter that grew under a measurement that changed, or that nothing vouches for, is reported instrument first: fix the instrument if it moved wrongly, raise only for a true number. (ratchet)
- A `batch` step with `counts: "vitest-json"` keeps the test files it knows green, from green runs and red runs alike, and passes them to the runner as `--exclude` on the next run. A red run followed by a fix runs the files that failed and what the fix reached, instead of the whole step. A file stops being known green when it changed, when a non-test file in its workspace or in a workspace it depends on changed, or when a root input changed. `--fresh` runs every file. (verify)

## 3.5.0

- With no `cf` it can find, the release runner and the doctor now name the version to add (plan 045). That is the newest `cf` past the repo's release-age gate, read from the registry and from `bunfig.toml` or pnpm's `minimumReleaseAge`. Before, the runner said only "the newest past the gate", and the doctor said nothing. The doctor FAILs a Workers repo with no `cf`, in the same sentence the runner refuses with.  `@geonosis/release` now exports the release-age policy reader (`releaseAgePolicyOf`, `youngerThan`, `newestPast`, `gateOf`), moved from the CLI. `@geonosis/base/node` exports `managerOf`, moved from the doctor. The cf and wrangler adapters share one reader of a version's bindings. (base, cli, doctor, release)
- `managerOf` reads `devEngines.packageManager` (npm's devEngines spec, a name with no version) after `packageManager` and before any lockfile, through an optional third argument. A bun scaffold declares `devEngines.packageManager: { name: 'bun', onFail: 'error' }`, so the doctor and the release runner know it is a bun repo before its first install, and its rule files name `bun run verify` from the start. (base, doctor, release, create-geonosis)
- An opt-in `i18n` preset: words come from the app's messages, never from literals in code (#137). - `no-literal-copy` reports a string literal under a copy name: a text-bearing attribute (`alt`, `title`, `placeholder`, `aria-label`, …), or a prop, key, variable or default named like copy (`label`, `title`, `*Label`, `*Message`, …). Ids, paths, URLs and class maps (`cva`, `tv`, `cn`) are not words. - `translator-namespaced` reports a translator called with no namespace. A root-scoped one types every key everywhere, and on a 2,513-key catalogue TypeScript gave up with TS2590. - oxlint's own `react/jsx-no-literals`, with `ignoreProps`, judges the text between tags.  `geonosis init` turns on the oxlint plugin a preset's rule names (`react` here). On a Medusa storefront's pre-i18n tree, `jsx-no-literals` alone caught 24% of 1,240 hand-found words, and the copy-name rule covers the attributes, keys and defaults it cannot. `no-literal-copy` leaves tests and stories (`testGlobs`), where the caller hands the words in. (cli, oxlint-plugin-biological-architecture)
- `geonosis update` bumps an npm tree with npm and runs the whole ritual there (#147). - `npm install` repins, and `npm ci --ignore-scripts` is the lock proof. `npm install --frozen-lockfile` ignores the flag, so it proved nothing. - `npm rebuild` runs the lifecycle scripts `install.allowScripts` names. - BY HAND, the doctor, `post-update` and the fast tier follow, as under bun, pnpm and yarn 1. - `--runtime npm` names it, and `update --prove` proves every claim under npm too. - The release-age gate reads npm's `min-release-age` (days) from the project `.npmrc`, then the user's, or `npm_config_min_release_age`. The refusal offers `--min-release-age=0` and says npm has no exclude list. - A refusing lifecycle script is named from npm's `> <package>@<version> <script>` header, and npm's lock-drift message gets the in-place recipe with `npm ci`.  Before, an npm tree got four install lines and nothing else, so a Workers + D1 consumer following them deployed without workflows 0.8.0's claims migration. (cli, release)
- `comment-ceiling` ships `capitalized-comments` as `['error', 'always', { ignoreConsecutiveComments: true }]`, and `geonosis init` writes a preset rule with the options the preset ships (#145). Before, the second line of every wrapped comment was a finding: 795 of 846 on a Medusa storefront. (cli, oxlint-plugin-biological-architecture)
- The oxlint counters `geonosis init` writes, and the ratchet's default oxlint command, pass `--report-unused-disable-directives-severity=error`. A disable directive that disables nothing is then an error the ratchet counts (#139). (cli, ratchet)
- `geonosis rules`, `init` and `post-update` keep oxfmt off the kit's rule files (#144). Where oxfmt would rewrite them, the directory goes into `ignorePatterns` in a plain-JSON `.oxfmtrc.json`, which oxfmt then reformats in its own shape, or into `.prettierignore` otherwise. oxfmt is asked again, and the line printed says which file changed. Before, the consumer met it as an `oxfmt --check` refusal at commit time, and formatting the files tripped the doctor's rules check. The doctor exports `checkFormatter`. (cli, doctor)
- A repository settles which rule files it receives in `geonosis.json → rules` (#132). `rules.omit` lists files it does not take; a copy the kit wrote is removed. `rules.overrides` maps a file to the repository's own text, written with the kit marker. `update`, `geonosis rules` and the doctor's `rules` check all read both. Before, an edited copy warned and the next update overwrote it. The gate command a rule file names is written in the repository's own package manager, so a bun repository reads `bun run verify`. An override that cannot be read, or that opens with no frontmatter, blocks the write by name. (cli, doctor)
- `create-geonosis` writes the kit's rule files with the gate named in the runtime it scaffolds (`bun run verify` for bun), as the doctor reads them once the lockfile exists. Before, a bun scaffold wrote `pnpm verify`, and after its first install the doctor's `rules` check WARNed that the files differ from what the kit ships. The doctor exports `ruleForManager`, and `kitRuleFiles` takes the manager. (cli, doctor, create-geonosis)
- `geonosis update`'s BY HAND holds only work a person has to do. A changesets line that records only a dependency move (`Updated dependencies [sha]` with its `- @geonosis/x@y` lines) is left out, because the update already made that move (#128). The run says how many it left out, and `bump-report.json` keeps them under `dependencyOnly`. A failed bump ends by naming its BY HAND count and where the list is kept (#127): a long FAIL above it no longer hides the list the failure is usually one of. post-update's notes line now says it speaks for the group, and that a moved floor's own changes are listed under BY HAND. (cli)
- `geonosis update` prints a moved package's changelog entries when the dependency came from a spec that is no version, such as a `file:` tarball (#118); before, it compared nothing and said "not readable". When an entry truly is missing, the line names the file it read. (cli)
- `geonosis update --dry-run` judges a floor against the commit, as the real run does. A pin the working tree already holds (left by an update that did not finish) is marked `(already in the working tree, not committed)`. A floor already at its target is listed as `unchanged`, no longer as a repin to itself (#108). (cli)
- `geonosis update` leaves a `file:` pin the author named as written and lists it under `left … — a file the author named, left as written` (#117). It used to repin any `file:` spec of a kit package to the registry version, so a tarball someone pinned on purpose to verify an exact build became the registry's version. A `file:` pin still moves when the last run wrote it from its own `--registry` directory, which the bump report now records resolved. (cli)
- A new check, `downgrades` (#139), asks every oxlint and eslint run a `verify` tier reaches, through scripts, turbo and package managers. - Does it fail on a disable directive that disables nothing? It is a FAIL with the count when the tree holds one, and a WARN when it holds none. - Which rules does it switch off? For eslint, a rule its own plugin's `recommended` config turns on. For oxlint, a rule its config names `"off"` and no layer enables. Each is WARNed with what it would report here, measured by raising it over the run's own targets.  Before, a consumer's ESLint config switched five react-hooks rules off (42 findings), and six dead `eslint-disable`s were left in the tree, with doctor PASS. (doctor)
- The doctor's `loaded` check reads the presets the law declares (`## Presets`, following a `CLAUDE.md` that imports `@LAW.md`) against the lint config (#136). - A declared preset whose rules the config does not enable is a FAIL. It names the rules and what each would report on the tree, measured beside a planted copy of the rule's own probe: a count the probe did not back is reported as unmeasured, never as a zero. - A choice the kit's law vocabulary does not have is a WARN, naming what is offered.  Before, a law saying `comments: ceiling` beside a config enabling none of it passed the doctor (380 + 846 findings when raised). `max-comment-density` now ships a probe, and every rule a law preset promises is held to one. (doctor, oxlint-plugin-biological-architecture)
- `exercised` asks a rule scoped by an override glob where that glob points (#142). A probe moved under `packages/ui/**` keeps its tier folder (`packages/ui/src/atoms/…`), and under `features/*/cells/**` it is anchored at `cells/`; before, only the basename moved, and a tier rule rightly ignored a file outside its tier. Probes that share a path run in separate oxlint passes, so no probe overwrites another. On a Workers + D1 app's config this took 26 false "fire nowhere" FAILs to none. (doctor)
- The doctor reads `stack.language: "ts"` and `"js"` as the languages oxlint reads (#141). A stack it does not read beside an oxlint config that loads the kit's plugin is a FAIL naming the config, never two SKIPs and a PASS: either the language is misspelled or the config lints nothing. (doctor)
- The kit's gates rule (`.claude/rules/geonosis/gates.md`) and the `geonosis-process` skill say how to run tests while working: - run only the test files the change touched; - a suite that already passed on this tree is not run again; - the gate at the end runs once per tree, since `geonosis-verify` answers a tier already green on this tree from its record; - a timeout under load is the machine, so wait it out rather than re-run into it. (doctor)
- The commit guard judges what the commit will hold, not only what is staged when it runs (#124). `git add <paths> && git commit` is judged on the index plus the paths that `git add` stages. Before, the paths it was about to stage were judged as not yet staged: an empty index went unjudged, and a stale one was refused. `git commit -- <paths>` is judged on those paths' working tree. `git commit -a` is judged on every tracked change. `git add -u` leaves untracked files out. `geonosis-ledger delivery check` gains the sources: `--cached [<path>…]`, `--paths <path>…`, `--all [<path>…]` and `--tracked`. (ledger, geonosis)
- G0 counts `.php`, `.phtml`, `.rb`, `.py`, `.go`, `.rs` and `.liquid` files as runtime code by default, beside the JavaScript and stylesheet kinds it already counted (#122). A PHP, Rust or Liquid repository's first tick was refused as shipping none. A refused tick now names the setting that decides it, `ledger.runtime.include` in `geonosis.json`, and the patterns in force. A test now holds the README's G0 block to the default, which it had drifted from. (ledger)
- `plan new` and `proof capture` accept a slug with the dots of a version (`1.2.0-everything`), as the geonosis-plan skill says (#123). A slug is lowercase letters, digits, hyphens and dots, starting and ending with a letter or digit, and the refusal says so. Both commands read one definition. (ledger)
- The oxlint unix-format reader counts a finding that names no rule, as oxlint prints an unused disable directive (`… Unused oxlint-disable directive … [Error]`). Before, the summary counted it and no line did, so a counter refused the run under `--report-unused-disable-directives-severity=error`. (lint-parity, ratchet)
- The README says who needs the server (#157): a client with no shell, where it is the only way in. A Claude Code repo with the geonosis plugin does not need it, and after a kit bump a running server refuses every call until the client reconnects it. (mcp)
- The PostHog sink is tested against posthog-node 5.56.0; its peer range is unchanged (`>=5.49.0`). (observability)
- `document-root-not-in-container`, in the `testbed` preset (#146). In a test file, an emptiness assertion (`toBe('')`, `toBeNull()`, `toBeEmptyDOMElement()`, …) on the `container` of `render(<X/>)` is reported when X's own module returns `<html>`, `<head>` or `<body>`. React 19 places those on the document, so the container is empty whether X renders or not. The rule parses X's module to know, and its fixShape points at `renderToStaticMarkup`. A Medusa storefront's nine global-error cases passed with the feature removed. (oxlint-plugin-biological-architecture)
- `LAW_PRESETS`: the choices a law declares under `## Presets` (`comments: rich | ceiling`, `state: controlled | zustand`, `tests: colocated | central`), each mapped to the rules it promises. A test holds the law template to it. The doctor reads it to judge a law against its lint config (#136). (oxlint-plugin-biological-architecture)
- `GEONOSIS_LINT_TIMING=1` adds the JS runtime's own clock to the report: how long it ran from its start to exit, how much of that passed before the plugin had loaded, how much in its rules, and how much outside any rule (#53). The JSON report carries `loadedMs` and `exitMs`. Before, the report named only the rules' time, and about 2 s of a 4 s overhead went unaccounted. (oxlint-plugin-biological-architecture)
- `public-path-exists`, in the `frontend` preset (#152). A string literal starting with `/`, whose first segment is a directory of the repo's public directory (`/assets/…`, `/images/…`), must name a file that is there. `next/image` and `<img>` fail only when the page renders. A route such as `/cart` is left alone, and `publicDir` (default `public`) is found by walking up from the file; with none, the rule is inert. A Medusa storefront had 27 such paths, every one a broken image. (oxlint-plugin-biological-architecture)
- `theme-tokens-declared`, in the `themekit` preset (#150): a colour utility (`bg-`, `text-`, `border-`, `ring-`, `fill-`, …) whose colour name starts with one of the repo's `prefixes` must name a `--color-*` token one of the `themes` files declares. Each theme file is found by walking up from the file being linted. Tailwind generates no CSS for an undeclared token, so the element is unpainted, and no build, type check or test says so. The finding names the declared tokens nearest it. Every string literal is read, so a class in a `cn()` ternary or a `cva` map is judged too. A Medusa storefront carried 23 such utilities across 11 files. A test or a story paints nothing and is left alone (`testGlobs`).  REFUSAL-IN-MINOR: `theme-tokens-declared` refuses a run whose `prefixes` or `themes` is not configured, once a run, and judges nothing until both are set. It is new in this release, so no config that extends `themekit` had it before: such a config gets the refusal on update and sets the two options. (oxlint-plugin-biological-architecture)
- `compound-must-be-stateless` names the shape for a compound's words (#138): a `labels` prop whose type sits in the compound's `.types.ts`, built by the caller, or by a pure builder beside the compound that takes the caller's translator and returns that declared type. `no-type-definitions-in-components` says to type such a builder against the declared type, never `ReturnType<typeof builder>`, which makes the types file and the component import each other. The `comment-ceiling` preset says what it checks, density and a capital at each comment's start, and that what a comment says is a review finding (#140). (oxlint-plugin-biological-architecture)
- `oxc-parser` is accepted at `^0.154.0` beside `^0.152.0 || ^0.153.0`. (oxlint-plugin-biological-architecture, ratchet, release, verify, verify-arch)
- `plugin-route-namespaced` accepts a plugin route under the plugin's own top-level namespace (`/<namespace>/…`, the `namespace` pin or the directory), the same as `/store/<namespace>/…`, so a repo no longer widens `scopes` to name it (#154). The `outsideScope` message no longer says Medusa does not serve such a route: Medusa serves every `src/api` route file, and the `/admin`, `/store` and `/auth` prefix decides only which CORS set and authentication apply (read in `@medusajs/framework` 2.19.0, `dist/http/routes-loader.js`). (oxlint-plugin-biological-architecture)
- `ssot-no-inline-facts` ships a probe, so the doctor's `exercised` can ask it under a repo's `owners` and override globs instead of reporting it SKIP as the kit's omission. On a Workers + D1 app's config, `exercised` now reads 60 enabled, 60 exercised. (oxlint-plugin-biological-architecture)
- When a run changes the tree it ran on, verify names the step and the files: `green not recorded: fast/10 changed scripts/gate-baseline.identity.json while it ran` (#130). Before, it said only "a step changed the tree". Verify watches the tree after each group of steps, and `@geonosis/ratchet` exports `treeFilesOf` and `filesThatMoved` beside `treeHashOf` to name the files. (ratchet, verify)
- The Cloudflare release runner and `migrations --record-bookmark` run Cloudflare's `cf` CLI and wrangler, each for what it does (plan 045, the user's instruction).  **Which tool runs what.** `cf` deploys a split or one version at 100 % (`cf workers deployments create`), reads what serves (`cf workers deployments list`), reads a version's bindings (`cf workers versions get`), and takes the D1 bookmark (`cf d1 time-travel get-bookmark`). Wrangler still builds and uploads the version (`versions upload`: in `cf`'s beta its deploy hands a bundled Worker to wrangler) and applies triggers. Every shape was measured against a live account on `cf` 1.0.0-beta.12.  What a Workers repo does: - **Install `cf`** beside wrangler, the newest version past the repo's release-age gate. With none found, the runner refuses before its first remote call; `release.cloudflare.cf` names another command. - **The account comes from the wrangler config's `account_id`**, or `CLOUDFLARE_ACCOUNT_ID`. - **A bookmarked database must be declared** in the wrangler config, because `cf` takes its `database_id`. - **Log `cf` in once**, with `cf auth login` (separate from `wrangler login`) or `CLOUDFLARE_API_TOKEN`. - **Telemetry is off on every `cf` call** (`DO_NOT_TRACK=1`). Each bookmark is recorded with `tool: "cf"` in the migrations envelope.  A `cf` call refused with code 10000 runs once more, as wrangler's does; a second 10000 names `cf auth login`. (release)
- A release runs from CI as it does from a laptop (#126). - `migrations --since <ref>` refuses a shallow checkout or a missing ref by name, with the fix (`fetch-depth: 0` and `fetch-tags: true`, or `git fetch --tags`), instead of a git error. - When `GITHUB_STEP_SUMMARY` is set, or `GEONOSIS_SUMMARY_FILE` names a file, `--record-bookmark` appends each D1 restore point to it, and `prove` appends its verdict, its steps and the version it uploaded, so they survive the runner. - A prove verdict carries `versionId`. - The README carries a GitHub Actions recipe: concurrency, an approved environment, an API token from secrets, telemetry off for `cf` and wrangler, and `.geonosis/` uploaded as an artifact. - A step that ran out of time while moving traffic names `cf workers deployments list` as the read of what serves. (release)
- A registry answer whose body does not arrive within the timeout is named as late (`answered 200, and its body did not arrive within 15000 ms`), not as "something that is not JSON". Measured on bun 1.4.3 and node: both raise a `TimeoutError` while the body is read. (release)
- The bookmark note and `migrations --help` name `cf d1 migrations apply <database_id> --dir <dir>` as the step after `--record-bookmark`. Measured against a live D1 database, `cf` and wrangler read the same bookkeeping table and name the same unapplied migrations (proofs/059). `--help` now names `cf d1 time-travel get-bookmark` as the command the bookmark runs; it had still said wrangler's. (release)
- A failed wrangler call leads with wrangler's own error, and the call that meets a just-refreshed OAuth token runs once more (#115, #116). - **The error first.** Every call reads the `command-failed` record wrangler writes to `WRANGLER_OUTPUT_FILE_PATH`, so the message reads `wrangler d1 time-travel info exited 1: … Authentication error [code: 10000] — <tail>`. Before, it ended with whatever wrangler printed last; on wrangler 4.147 that is the OAuth scope warning, which hid the cause. - **One more run on code 10000.** A call answered with code 10000 (Cloudflare's authentication error) runs once more. Measured on 4.147: the call that refreshes an expired OAuth token was refused, and the same call seconds later passed (2 of 5 runs). A second 10000 stops, saying a refresh did not fix it and that `wrangler login` signs in again. Any other failure runs once. (release)
- A red step that failed on the network is marked as an environment failure (#121). DNS (`getaddrinfo ENOTFOUND`, `EAI_AGAIN`), `ECONNRESET`, `ETIMEDOUT`, an unreachable network, and wrangler's "Unable to resolve Cloudflare's API hostname" all count. The step stays FAIL, the line under it says "an environment failure, not a code fault — re-run it", and the gate report carries the line that named it as `environment`. A local server that never started (`ECONNREFUSED`) is not one.  The Stop hook does not block on a red that is only the network: it runs the tier again. A red it does block on names the run it came from: when it started, how long ago, and over which commit. While a newer heavy gate is running in the same repository, an older red report is not replayed as current. (verify, geonosis)
- A tier runs once per tree. A tier already green on this tree with the same flags (`--affected`, `--require-work`) is answered from its green record in milliseconds, naming the tree and when it went green, and runs nothing. `--fresh` runs it again. The green record now stores the flags it ran with, and `greenOnThisTree(cwd, tier, asked?)` can match them. Before, the record existed but only a gate daemon consulted one, so a Stop hook or an agent re-proved an unchanged tree at full cost every time. Through a gate daemon, `--fresh` reaches the run the daemon starts, so it runs the tier instead of being answered from the record. (verify)
- `geonosis-verify <tier> --retry-environment-failures <n>` re-runs a step up to `n` times when its failure is only the network: DNS, a reset or a timeout, as #121 classifies it (#125). A red that is the code is never re-run. Each re-run is named in the run's output and recorded in the report as `retried: { after, times }`. A release script whose verify calls a third-party API can take it, so a DNS blip does not abort a release before anything deploys. It defaults to 0 and passes through the gate daemon. (verify)
- `geonosis-verify prove` takes `--jobs` as a tier does (#129); it refused the flag before. `--help` and the README name the whole order behind the default: `GEONOSIS_JOBS`, `GEONOSIS_HEAVY_SLOTS`, then `geonosis.json → caps.heavy`, then cores and memory. `caps.heavy` is the one key that caps every entry point: a tier, `prove`, the Stop hook and the `GEONOSIS_SLOTS` a step is handed. It went unfound because the help left it out. (verify)
- `--require-work` counts a source file a test reaches by naming the file it is built to (`resolve(ROOT, 'dist/stub.js')`), as it already did for an import of a `dist/` path. A stub a bin harness names by its built path was reported as reached by no test. (verify)
- The walk launches Playwright's full Chromium (`channel: 'chromium'`), not its headless shell (#155). Under Playwright 1.64.0 and Chromium 156 the shell grants WebGL that asks `failIfMajorPerformanceCaveat` in SwiftShader even under `--disable-gpu`, so a walk saw 3D no visitor without a graphics processor gets. `walk.gpu: false` and `--no-gpu` walk without one; the WebGL caveat is then refused, as Chrome refuses it. `npx playwright install chromium` installs the full build beside the shell. (walk)
- The syntax guards check a file with the oxlint of the workspace it is in (#119). A write under `apps/web/` reads `apps/web/node_modules/.bin/oxlint`, and so on up to the repository root, before the root's and PATH's. Before, a workspace with its own oxlint got "no parser here" on every write. When no oxlint is found, the message says where it looked. (geonosis)
- The Stop hook starts no gate while another heavy gate, a `pnpm release` or a `verify`, is running in the same repository (#120). Before, it waited only when every heavy slot on the machine was held. A release's verify holding one slot of three raced the hook's own run over the same tree, and a 4-second step took 320. Beside a gate in another repository it still launches while the machine's budget has a slot. The hook names the run it is waiting on and tries again at the next turn end. (geonosis)

## 3.4.1

- `geonosis update` and `post-update` name a package the kit moved under a subpath of another (D-118). A dependency on `@geonosis/buketi-ui` (or the older `@geonosis/storefront-ui`) is a BY HAND line that names both the dependency and the import prefix: `package.json dependencies @geonosis/buketi-ui → @geonosis/design-kit@<version>: change the dependency to @geonosis/design-kit, and every import specifier @geonosis/buketi-ui/… to @geonosis/design-kit/buketi-ui/…`. The version is the one the release records for `@geonosis/design-kit` in `release-kits.json`, and the old package is never repinned. (cli)
- `geonosis update` runs the post-update of the copy it installed in a workspace that declares the CLI when the repository root declares none (#76). It used to resolve the CLI from the root only, fall back to the copy running the update (the version being left) and report `OK`. Where the CLI is declared and no copy at the target is installed, the step now fails, or is reported as not run when the install was skipped, and names `geonosis post-update --from <old>`. The doctor step reads the same workspace copy. (cli)
- `geonosis update` over a tree already on the target says so instead of listing no-op repins (#108). `--dry-run` lists only the pins that move, judged from the commit (a bump not yet committed still shows `1.3.0 → 1.4.0`), and prints `already at <v> — nothing to repin` when none do. A real run ends there, installing nothing, when the last update to that version finished green. After a run that did not finish, which leaves every pin at the target, it runs the steps again and says why: `the last update to <v> did not finish — running its steps again`. (cli)
- The doctor finds the reader of a `geonosis.json` block through a door declared in any workspace, not only at the root (#80). A `verify` block in a monorepo whose `apps/<pkg>` declares the `geonosis` metapackage (which brings `@geonosis/cli`, which brings `@geonosis/verify`) is `OK`, where it was `WARN … nothing reads it` while `geonosis verify` read it. The warning for a reader installed with no block still names `geonosis explain <name>`, and now also the explicit way to keep the defaults: `"<block>": {}` in `geonosis.json` says they are meant, and silences it. (doctor)
- @geonosis/base@3.4.1 - @geonosis/ratchet@3.4.1 (evals, ledger, lint-parity, rails, release, verify)
- @geonosis/base@3.4.1 (observability, ratchet, review, testbed, themekit, verify-arch, walk)
- `list-take-without-order` no longer reports a Drizzle select whose every field is an aggregate and that has no `.groupBy()` (#106): `db.select({ n: count() }).from(t).where(…).get()` is one row, so its order cannot matter. `count`, `countDistinct`, `sum`, `sumDistinct`, `avg`, `avgDistinct`, `min`, `max` and an `sql` template opening with an aggregate count. A select that mixes a column in, or groups, is judged as before. (oxlint-plugin-biological-architecture)
- `cell-must-not-compose-cell` no longer reports a component folder's own barrel (#112). `cells/TestimonialSection/index.ts` re-exporting `./TestimonialSection` and its types is that component, like `x.parts.tsx`, not composition. The exemption covers an `index.ts` (not `.tsx`, which can render) for what it takes from inside its own folder; a folder barrel that reaches another cell is still reported, which keeps the laundering checks of #4 and #11. (oxlint-plugin-biological-architecture)
- `no-runtime-dead-affordance` never judges an atom its map draws from `input`, `select` or `textarea`. A form field holds its value and its form submits it with no handler, so it is not a dead affordance. A repo that hands the rule the whole element→atom map `no-raw-html-atoms` takes, which is the natural thing to do, no longer gets every handlerless `<Input>` reported: 7 findings over one consumer's text fields, every one false. A button-like atom in the same map is judged as before. (oxlint-plugin-biological-architecture)
- A release runner that could not start is now reported as one (#107). A runner file without the executable bit used to read as `the runner closed before answering "upload"`, as if it had crashed. The spawn error is now named with the fix: `could not start the runner <path>: spawn EACCES — is it executable (chmod +x, with a #!/usr/bin/env node line), and is the path right?` (release)

## 3.4.0

- `geonosis release prove --runner` gives each runner step its own time limit (#105). Every step was capped at 60 s, so a promote that took effect at 61 s was reported as a failed release while production served it. The defaults are now upload, promote and rollback 300 s, smoke and ramp 60 s (a ramp adds its hold). `release.proof.timeouts.<step>` sets any of them in milliseconds. A step that moves traffic and runs out of time says its state is unknown, names the version that may already be serving, and says how to read what is live before running the release again. (release)
- The `drift` check reads a marker that opens inline code in Markdown (`` `geonosis:snapshot: …` ``) as a mention of the marker, not a use; a template literal in code is still a use. The doctor had failed on its own changelog. (doctor)
- `geonosis-doctor` has a `once` check, and `geonosis post-update` a step of the same name, for a repo that adopted the ratchet before `--record` (#51). A scaffolded repo runs `tsc`, `oxlint` and `vitest` once per gate; an existing one kept running each as a step and again inside the counter that reads it, and nothing told it how to switch (one `yarn verify` took 361 s). The check reads `geonosis.json`, the root `package.json` scripts and `geonosis.ratchet.json`, and WARNs where one gate holds a ratchet step that measures and a step that runs the same tool over the same files as a counter with a `command` and no `output`. The line names the step to replace with `geonosis-ratchet --record <counter>` and the `"output"` to add to each counter; `post-update` prints it as a BY HAND line. `recordedOutputOf` and `recordCommandOf` in `@geonosis/base` are where `init`, `create-geonosis` and the check take the recorded file and the record step from. (base, cli, doctor)
- A command run from the repo root now uses the workspace's own `wrangler` and `oxlint` (#94). When a tool was a devDependency of `apps/website` only, `release migrations --record-bookmark` failed with `Executable not found in $PATH: "wrangler"`, and the doctor's `exercised` check failed the same way for oxlint. Both now start at the directory of the config they read (the wrangler config, the `.oxlintrc.json`) and walk up to the nearest `node_modules/.bin`. PATH is used only after that, and a `release.cloudflare.wrangler` or `--oxlint` you name still wins. The Cloudflare runner looks for wrangler the same way. The lookup is `binNear` in `@geonosis/base/node`. (base, doctor, lint-parity, release)
- The doctor reads `.oxlintrc.json` the way oxlint does, with comments and trailing commas (#90). A config explaining why a rule is off no longer fails `loaded` with `could not read it`, and `exercised` and lint-parity read it the same way. `parseJsonc` now lives in `@geonosis/base`; `@geonosis/release` re-exports it unchanged. (base, doctor, lint-parity, release)
- A kit bin starts on bun when bun is on PATH and falls back to Node when it is not (#52). An npm repo with no bun used to get `env: bun: No such file or directory` and exit 127 from a bin that needs only the Node its `engines` declares. The shim is now a POSIX shell line ahead of the same JavaScript: it `exec`s bun when it finds one, otherwise it says once on stderr that it runs on Node and how to get bun, and `exec`s Node. Under `npx`, `npm exec`, `pnpm exec`, `node_modules/.bin` and a direct path it adds about 3 ms to a start on bun. Windows stays a non-goal. The doctor's `tool runtime` warning no longer says a machine without bun cannot start the bins. (cli, doctor, evals, ledger, lint-parity, mcp, observability, rails, ratchet, release, review, testbed, themekit, verify, verify-arch, visual-diff, walk, create-geonosis, geonosis)
- A BY HAND changeset may name when it applies: `BY HAND (from <3.2.0):` for a repo that came from below 3.2.0, `BY HAND (done by rules, check):` for an action those `geonosis post-update` steps do, both joined by `;`. `post-update` counts an entry `--from` rules out, or whose steps passed earlier in the same run, and does not list it: `1 BY HAND item does not apply from 3.2.0`. An entry with no condition, or one this version cannot read, is listed as before (#77). (cli)
- `geonosis update` and `geonosis explain` read `.oxlintrc.json` the way oxlint does, with comments and trailing commas (#90). The lint step of an update took a commented config for one that does not load the plugin and reported `ran: false`, passing; it now counts that config's findings. `explain` no longer reports a commented config's rules as defaults. (cli)
- `geonosis update` run from inside a workspace package acts on the workspace root (#91). It no longer takes the package for the root, finds no lockfile and leaves the other workspaces' pins behind with a PASS. The root is the nearest directory above holding `pnpm-workspace.yaml`, a `package.json` with `workspaces`, or a lockfile; the run prints `workspace root: <dir>`. With none found and no `--runtime`, the refusal names the directories it looked in. (cli)
- `geonosis update` classifies a `yarn.lock` by what it holds (#103): the `# yarn lockfile v1` header is yarn 1, a `__metadata:` block is Yarn 2+. An empty `yarn.lock`, or one with neither, is refused saying so and naming `yarn install` as the next step. It is no longer called a Yarn 2+ lockfile "with a `__metadata` block". (cli)
- `geonosis update` counts oxlint findings before and after whenever the bump moves the group, not only when a manifest names `@geonosis/oxlint-plugin-biological-architecture`. A tree that declares only another member and loads the plugin through a hoisted install had the count skipped with `this bump did not move @geonosis/oxlint-plugin-biological-architecture`, and passed over the new plugin's findings. (cli)
- `geonosis post-update` and `geonosis update`'s commit body name the release notes as `node_modules/@geonosis/cli/GROUP-CHANGELOG.md` when that link is the copy that ran, and the real path only when it is not. Under pnpm the real path is the virtual store's, whose directory name carries the peers' versions and changes when one of them moves (#78). (cli)
- `geonosis post-update` records the kit version it ran for in `.geonosis/post-update.json`, and `geonosis-doctor`'s `rules` check WARNs when that record names an older kit than the one installed, or is missing where `.geonosis/bump-report.json` says an update ran: `` `geonosis post-update --from <version>` has not run here for kit <installed> ``. A correction to 3.3.0, whose CLI README and #44 entry said `geonosis update` runs the copy it installed for an update from any older version: that holds for an update run by 3.3.0 or later. An update run by 3.2.0 or older runs its own steps, with no post-update, no BY HAND lines and no oxlint count, and still says PASS; the doctor that runs after it is the new version's, so it is the one that says so (#76). (cli, doctor)
- `geonosis update` now catches a bump that splits one of the repo's packages into a second snapshot (#93). This happens when the kit brings a second version of an optional peer. In a Medusa tree, a second `yaml` forked vite, then `@medusajs/framework`, then `core-flows`, so every workflow registered twice and the backend refused to boot. The update had said `SKIPPED build — the lockfile moved only what this bump repinned`. It now compares pnpm's snapshot keys as well as versions. A package whose peers resolve differently counts as moved, the build runs, and the report names it:  @medusajs/core-flows@2.19.0: 1 → 2 snapshots (new peer yaml@2.9.1)  `bump-report.json` carries the splits in `lockSplits`. The kit's own `yaml` range now starts at `^2.0.0`, the oldest version its tests pass against, so pnpm reuses the `yaml` a repo already has instead of adding a second one. (cli, ratchet)
- Four `drift` false positives a consumer measured. A script that builds the file it runs earlier in the same command (`--outfile`, `-o`, `--outdir`, `tsc` with an `outDir`, or the workspace's own `build` script) is OK, naming the segment (#79). A root declaring only `geonosis` reaches the tools through `@geonosis/cli`: doors are followed through installed dependencies, and the "stale install" clause is printed only for a package on no dependency path from a declared one (#80). `geonosis:snapshot: <YYYY-MM-DD> <reason>` marks a generated file whose source is gone: SKIP naming the date and reason, and the generated-file FAIL names both markers (#82). A test file whose relative imports do not resolve where it sits is UNJUDGED naming the first one, not an orphan FAIL (#83). (doctor)
- A `--only` run that judged nothing no longer prints `doctor PASS` (#92). A named check that returns no line is UNJUDGED and exits 1, naming the check; `--only release` over a Workers repo used to print `0 checks` and PASS. The `release` check now names the missing key: a repo with `release.workers` and no `release.wrangler` gets a WARN listing the wrangler configs it found and did not read, and a wrangler config with no Worker declared gets a SKIP. A full run is unchanged. (doctor)
- The `runner` check prints one finding per cause (#75, #81). Commands that run one bin through one package runner after another command become one WARN that lists every counter or verify step it covers. Workspaces whose test scripts get the same warning are listed on one line. The advice now keeps PATH unchanged (#74): it recommends `env PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/<bin>` and says that dropping the prefix makes a bare-name spawn fail with ENOENT. A vitest workspace counts as proven, and gets an OK naming the step, when a verify step that runs its tests carries `proves: { plant, expect }` (#81). The `@cloudflare/vitest-pool-workers` clause appears only when the workspace's vitest config loads that pool, directly or through a local file the config imports. (doctor)
- A Pages project is no longer judged as a Worker (#84). The doctor's `workers` check now gives a Pages config a SKIP where it used to WARN about a missing `observability` block, and FAILs one that has the block, because `wrangler pages deploy` refuses it. `geonosis-release-cloudflare` exits 2 when the config it would upload is a Pages project. (doctor, release)
- Every reader of a config oxlint or wrangler reads as JSONC now reads it as `@geonosis/base` does (#90). `probelessRules` and `unreachableTierRules` no longer refuse a `.oxlintrc.json` with comments or trailing commas, and the MCP preamble names its rules. `deriveLocalOnlyConfig` reads a wrangler config with block comments and comments after a value, and keeps a `,}` inside a string. The lint plugin's own reader no longer strips the comma from a string holding `,}`. `testbed` and `evals` declared `@geonosis/base` as a devDependency while importing it; it is now a dependency. (evals, mcp, oxlint-plugin-biological-architecture, ratchet, testbed)
- `backend-d1` now holds only what is true of every D1 Worker: `list-take-without-order` and `no-d1-transaction` (#89). `worker-handles-are-scoped` moves to a new preset, `backend-cells`, for a Worker that opens a database-cell handle such as a pool, a Hyperdrive client or a shard. A plain `env.DB` binding has no handle to open or close. Taking `backend-d1` as documented used to fail the doctor with a rule that judged nothing.  BY HAND (from <3.4.0): `.oxlintrc.json` lists its rules, so nothing changes until you act. On a plain D1 binding, remove `worker-handles-are-scoped`. If your Workers open a cell handle, keep it with its `within` and `wrapper` options, and name `backend-cells` wherever your repo records its presets. (oxlint-plugin-biological-architecture)
- `list-take-without-order` can now read uniqueness from the repo's Drizzle schema (#88), so it no longer needs a flat copy of it. A new option, `schema`, names the schema modules; each path is found walking up from the linted file. The rule reads them with oxc-parser:  - a column marked `.primaryKey()` or `.unique()`; - in the table's third argument, `primaryKey({ columns })`, `unique().on(…)` and `uniqueIndex(…).on(…)`.  Uniqueness is per table. A composite pins a read only when every one of its columns is `eq`-pinned under `and(…)`. So `eq(activations.siteUrl, x)` on a table whose unique is `(license_id, site_url)` is still reported, which a flat `uniqueColumns: ["site_url"]` could not say. Import aliases are followed, and relational `db.query.<table>.findFirst` reads its table from the query. Raw SQL is pinned through the same schema's SQL column names. `uniqueColumns` stays, as a flat list for a repo with no schema to read. The plugin now depends on `oxc-parser`, at the range the rest of the kit already installs. (oxlint-plugin-biological-architecture)
- `list-take-without-order` now judges Drizzle's `.get()` on D1 (#87). In drizzle-orm/d1 0.45, `D1PreparedQuery.get()` is `stmt.bind(…).all().then(({ results }) => results[0])`. It adds no LIMIT, so it reads every row the select matches and returns whichever came first. That is the raw binding's `.first()` bug, plus the cost of reading every match. A select chain ending in `.get()` with no `.orderBy()` is reported unless `eq` on a unique column pins the row. The fix is `.orderBy(…).limit(1)`, or a unique pin. An insert, update or delete `.returning().get()` returns the row it wrote and is not judged. A `.limit()` with no order keeps its own finding. (oxlint-plugin-biological-architecture)
- New counter, `mirroredRowTypes` (#73). It builds the D1 schema the way wrangler does: every sqlite `release.migrations` directory, applied in wrangler's order to an in-memory SQLite. It then counts the TypeScript types kept by hand beside a table, and cites each one with the `$inferSelect` type that replaces it. The `geonosis-backend` skill now names Drizzle's D1 driver as the default data layer and says what `backend-d1` judges on Drizzle and on the raw binding. It also carries the probed recipe for generated migrations, including the baseline for an applied hand-written history. (ratchet)
- `geonosis-ratchet --raise <key>=<n> --because "<why>"` raises one baseline. It measures that counter with its tier and params and writes `<n>` and the counter's identity only when the counter reads exactly `<n>` and `<n>` is above the baseline. It refuses with exit 2 any other reading, a fall, a key the config or the baseline does not hold, a blank reason, and a counter that cannot measure, and it prints the sentence for the commit message. The ratchet's FAIL for a grown counter, `--prove` (a raise above the reading must be refused), the plugin's baseline refusal and the `gates` rule file name it. In a session under the plugin, a command that runs `geonosis-ratchet` with `--raise` asks the person first, as does a command too long to read that names both. (ratchet)
- `HEAVY_SLOT_HELD` (`@geonosis/ratchet`) and `BASE_ENV` and `SLOTS_ENV` (`@geonosis/verify`) name the variables a verify tier hands its steps (`GEONOSIS_HEAVY_SLOT_HELD`, `GEONOSIS_BASE`, `GEONOSIS_SLOTS`), so a repo whose test run must not inherit them can list them from one place. (ratchet, verify)
- `geonosis-release smoke run` reads the consumers of a sweep at the same time, as many at once as the heavy budget has slots, and prints them in declaration order. With one slot (`GEONOSIS_HEAVY_SLOTS=1`) they are read one after the other, as before. A consumer that cannot be run starts no further one; those already running finish. (release)
- `geonosis release migrations` now reads only migrations under `mikro-orm-ts` (#95). It used to read and "lint" every added `.ts` under the dir, so route tests and library modules counted as migrations. A release that added no migration could print `OK (6 read, 6 linted)` where it should have said `NONE … nothing was read`. A file counts only when it declares a class extending `Migration` imported from a mikro-orm module, which is the class MikroORM runs; it is read with oxc-parser. A file that does not parse is still read and reported. An OK line now lists each file it read, so a reviewer can see their migration was judged. (release)
- A failed release step now shows the runner's own words (#104). `geonosis release prove --runner` piped the runner's stderr and never read it. So when a step was refused, wrangler's `✘ [ERROR]` line was lost, and the only record was the runner's reply, which a hand-written runner may have cut. Prove now drains the runner's stderr, waits for the runner to exit once a step is refused, and prints the last 40 lines under the REFUSED line:  REFUSED after upload: … — the runner said: Command failed: …/wrangler versions uploa the runner's stderr ended with: ✘ [ERROR] A request to the Cloudflare API (…/versions) failed. Authentication error [code: 10000]  A runner that stops answering gets the same tail, appended to its error. (release)
- `geonosis-verify <tier> --affected` diffs against a mark of that tier's own. The `fast` tier keeps `refs/geonosis/verified`; every other tier keeps `refs/geonosis/verified-<tier>`, moved only by a green run of that tier and only when no lint input differs from HEAD. With no mark a tier other than `fast` runs every workspace, and its first green run sets the mark. `GEONOSIS_BASE` is the base of the tier being run. (verify)
- A tier that goes green records the hash of the tree it ran on in `.geonosis/green-<tier>.json`, and `greenOnThisTree(root, tier)` answers that record only while the tree still hashes the same. A red run, a run limited by `--since`, and a run whose steps changed the tree write no record. (verify)
- `--affected` no longer reads a version cut as a change to every workspace. A `package.json` whose only difference from the base is `version`, any `CHANGELOG.md` or `GROUP-CHANGELOG.md`, and each file listed under `versionFiles` in `geonosis.json` whose only difference is a version number select nothing. A dependency range or script that moved beside the version still selects its workspace. (verify)
- `create-geonosis` now reads `.oxlintrc.json` as JSONC (#90), like every other reader in the kit and like oxlint itself. A config with comments or trailing commas has its enabled rules counted, instead of reading as no rules at all. The plugin's SessionStart digest reads the same config through bun's own JSON5 parser, so it agrees with the MCP preamble on a commented config. (create-geonosis)

## 3.3.0

- An install the kit starts tells the packages it installs where it runs. `startedIn(dir)` in `@geonosis/base/node` sets `INIT_CWD` to that directory, and the scaffold's install, `geonosis update`'s commands and the smoke's phases all run through it. A package's install script reads `INIT_CWD`, and lefthook's installs git hooks there: a scaffold or an update started under `pnpm` inherited the caller's `INIT_CWD`, so hooks landed in the repository that ran the command — the kit's own, twice, from its tests. (base, cli, release, create-geonosis)
- The metapackage declares the `geonosis` command itself (#68). pnpm links only the bins of a direct dependency, so with `pnpm add -D geonosis` alone the documented door did not exist; `npx` found it only because npm hoists. `geonosis` now ships `bin/geonosis.mjs`, which runs `@geonosis/cli`'s command through its new `@geonosis/cli/bin` export, and answers exactly as the cli does. (cli, geonosis)
- BY HAND (done by rules, check): if you updated to 3.2.0 from an older version, run `geonosis rules` once. The updater that ran was your old version's, which has no rules step, so `.claude/rules/geonosis/` was never written and the update still said PASS. `geonosis-doctor` names the same command when the directory is missing. (cli)
- `countersFor`, which `geonosis init` and `create-geonosis` write counters with, gives a bun repo one `testFailures` counter per workspace, as it gives a pnpm repo: `bun run --filter <name> test`, reading the `.geonosis/vitest.json` that workspace's test script writes. It returned none for bun, so the report was written and nothing read it, and the doctor warned about it four times over a fresh scaffold. (cli)
- The doctor's `tool runtime` advice is the manager's own. Measured on a tree with bun off the PATH: pnpm (11.8.0, 12.10.1) installs what `devEngines.runtime` names and puts it on the PATH of `pnpm exec` and scripts; yarn 1.22.22 and npm 11.13.0 read it for nothing, so the line says `yarn add -D bun@<range>` or `npm install -D bun@<range>`, which links `node_modules/.bin/bun` onto the PATH of `yarn <bin>`, `npm run`, `npm exec` and `npx`. The manager is read from `packageManager`, else the lockfile; where neither names it the line gives all three. A yarn or npm tree that declares bun only in `devEngines` is warned, a bun devDependency in any workspace is OK, and the doctor reads the range from an installed kit's `devEngines.runtime` as well as the `engines.bun` earlier releases declared. `geonosis update` prints the same line as a BY HAND item in its new `runtime` post-update step, and the CLI README carries the table (#52). (cli, doctor)
- The gates rule every session loads says a tool the ratchet counts runs once, in the fast tier, as `geonosis-ratchet --record <counter>`, and that the ratchet reads the recording and runs nothing; it no longer describes the gate as typecheck, lint and test followed by a ratchet that repeats them. `geonosis rules` and `geonosis update` write the new text. (cli)
- `@geonosis/cli` ships `GROUP-CHANGELOG.md`, which every consumer has: the cut writes it once into `packages/cli`, and `geonosis` no longer carries a copy. `geonosis update` prints from it the number of entries between the old and the new version, every entry whose text starts with `BY HAND:`, and the path of the file; the suggested commit body names that path (#33). A changeset whose text starts with `BY HAND:` is how an author marks an action no command can do for the consumer. (cli)
- A parse check that did not finish no longer reads as a file that parses (#49). `pre-edit` allows the edit and tells the model, in the hook's `additionalContext`, that the file was not checked; a new `PostToolUse` hook, `post-parse`, registered with `asyncRewake`, then checks the file as written with eight times the budget and wakes the model with the diagnostic when it does not parse, or with the fact that it could not check either. `post-bash` does the same for what a command wrote. Every note a PreToolUse hook allowed with (`no parser here`, `the ledger ran past its budget`) now reaches the model: Claude Code sends the stderr of a hook that exits 0 to the debug log only. (cli)
- The published manifests no longer declare `engines.bun`, which yarn 1 warned about once per installed package (`The engine "bun" appears to be invalid`). A package that starts its bins on bun now names it in `devEngines.runtime`, the same list the kit's root declares; measured on yarn 1.22.22, npm 11.13.0, pnpm 11.8.0 and 12.10.1 and bun 1.4.2, none of them warns on that field in a dependency. A test over every published manifest holds that `engines` has only keys yarn 1 knows and that the list equals the root's, and a yarn 1 install of those manifests prints no engine line (#46). (cli, doctor, evals, ledger, lint-parity, mcp, observability, rails, ratchet, release, review, testbed, themekit, verify, verify-arch, visual-diff, walk, create-geonosis)
- A repo `geonosis init` or `create-geonosis` leaves behind runs each tool once per `geonosis-verify full` (#51). Its fast tier is `geonosis-ratchet --record typecheckErrors`, `--record oxlintErrors` and `--record testFailures` (`pnpm exec`, or `bunx` on bun), and the counters name the files they read (`output`), so the ratchet step that follows runs none of them again. The typecheck counter runs the repo's `typecheck` script when it has one (`tsc --noEmit` when it has none); the root test counter prints the runner's own report as well as writing the JSON. `pnpm ratchet` on its own, with nothing recorded, refuses and names the record command, as it does for any counter that reads a recording. `tiersFor` takes `recorded`, the counters a fast step records (all three by default), and `countersFor` takes the repo's script names. (cli)
- A repo whose `release.migrations` names the postgres or mikro-orm-ts dialect is told squawk-cli is missing before a gate needs it (#71): the doctor's `release` line FAILs and `geonosis update` prints a BY HAND line, both with the gate's own message and its exact install command. squawk became an optional peer in 3.2.0, so an update from 2.x left it installed until the next reinstall removed it, and the first sign was a ratchet counter that could not measure. (cli, doctor, release)
- The Stop hook replays what it already knows about the tree — a fresh red report, the last judged run, a finished background run — before it looks for `geonosis-verify`. It looked first, so a repo where the binary is not on the hook's `PATH` let a turn end over a red report describing that very tree. (cli)
- `geonosis update` under a release-age cooldown (pnpm `minimumReleaseAge`, bun `minimumReleaseAge`) checks the third-party packages the kit's packages declare, once the kit's own packages are exempt or the manager's override is in `--install-args`. It stops with exit 2, before writing, and lists each package whose newest release satisfying the declared range is younger than the age: under an exemption that means no old enough release satisfies the range, under an override it means the newest one is young, because the override lifts the age for every package. The README's "Under a release-age cooldown" holds the exemption each manager has, measured: pnpm `minimumReleaseAgeExclude` takes `'geonosis'` and `'@geonosis/*'`, bun `minimumReleaseAgeExcludes` takes exact names and ignores a glob, npm has no per-package exemption. The kit's cut holds what it publishes to the same age: `release:check` refuses a range in a publishable manifest (`dependencies`, `optionalDependencies`, required `peerDependencies`) that no release at least `release.thirdPartyAge` days old (default 7) satisfies, and names the range that would. The published floors of `oxc-parser`, `pixelmatch`, `@modelcontextprotocol/sdk` and six `@radix-ui` packages are lowered to a release that old, with the lockfile still resolving the latest. The doctor's version reader now reads a partial version (`>=19`, `^1`) and an `||` list of such ranges (#67). (cli, doctor, mcp, ratchet, verify, verify-arch, visual-diff)
- `geonosis update` names a dependency on a package the kit renamed. A tree on `@geonosis/storefront-ui` read "left … its own version line, not this one" and `update PASS`, and stayed on a name the release replaced with `@geonosis/buketi-ui`. The update, `--dry-run` and the commit body now carry a BY HAND item, `package.json dependencies @geonosis/storefront-ui → @geonosis/buketi-ui@1.0.0: change the dependency and every import specifier`, with the version the release records in `release-kits.json`; `bump-report.json` carries it in `byHand`. The new `geonosis post-update` step `replaced` prints the same from the installed copy, so a repo updating from a version that predates this is told too. The renames are one map, `RENAMED_PACKAGES`, which the law-line check (`REPLACED_BY`) now includes, and a test holds that each new name is a package this workspace publishes (#44). (cli)
- `geonosis update` says why the build step did not run in one sentence (no build tier declared) and reports what the lockfile moved beyond the repinned packages apart, in `lockMoves` of the bump report and in a `lockfile` line (#35). A package is the kit's own dependency when it is reachable from a repinned package in the lockfile's dependency graph; a platform family folds into the package whose `optionalDependencies` list it (`oxc-parser ×22`); a package is named only when a manifest of the repo declares it, and the rest is counted. The lockfile is read for pnpm, bun and yarn 1. (cli)
- `geonosis update` hands its post-install work to the copy of `@geonosis/cli` it just installed: it runs that copy's `geonosis post-update --from <version>`, and falls back to its `geonosis rules` for a version that predates it, so a step a later release adds reaches a repo updating from any older version, from that release on. Until now the code that ran was the old version's: updating to 3.2.0 from 2.12.0 reported PASS and never wrote `.claude/rules/geonosis/` (#31). `post-update` writes the rule files, checks them with the doctor's `rules` check and exits 1 when they are not what the kit ships, so an update cannot report PASS where the doctor would warn about them. The report names the step `post-update`, in place of `rules`. The doctor's `rules` advice names `geonosis rules` first and `geonosis update --to <version>` only when the copies were written by a newer kit than the installed one; `compareVersions` moves from the CLI to `@geonosis/doctor`. (cli, doctor)
- `geonosis update` runs oxlint over the tree when the bump moves `@geonosis/oxlint-plugin-biological-architecture`, whatever the repo's `fast` tier runs. It counts the errors of each `.oxlintrc.json` that loads the plugin before the repin and after the install, and an error the old plugin did not report FAILs the update with a BY HAND item naming the count and the rules; a run of the same bump again keeps the count the first run measured, so a tree still red is not passed the second time. The update said `PASS` over 13 new findings in a tree whose `fast` tier ran ESLint only (#45). (cli)
- `geonosis update` and `update --dry-run` stop with exit 2, before writing anything, when a package they would install is younger than the release age the consumer's manager holds back (#38). bun's is `[install] minimumReleaseAge` (seconds) in the project's `bunfig.toml`, `$XDG_CONFIG_HOME/.bunfig.toml` or `~/.bunfig.toml`; pnpm's is `minimumReleaseAge` (minutes), read with `pnpm config get`. The message names the policy and the file it is set in, each held release's publish time from the registry and the date it becomes installable, and the two ways the user may choose to install it now: `--install-args "--minimum-release-age 0"` (bun) or `--install-args "--config.minimumReleaseAge=0"` (pnpm), or the manager's exclude list (`minimumReleaseAgeExcludes`, exact names, in bunfig; `minimumReleaseAgeExclude` in `pnpm-workspace.yaml`). The kit never overrides the policy; an override already in `--install-args` is the user's choice and the check steps aside. Packages the exclude list names are not held. npm's `min-release-age` is not read: neither bun 1.4.2 nor pnpm 11.8.0 applies it (measured), and `update` never installs with npm. (cli)
- A `release` check asks what the release contract asks of a Workers repo, from the wrangler configs `release.wrangler` names (#63, #64, #65). It FAILs a Worker with no `version_metadata` binding while `proof.mustAssertVersion` is on, and WARNs a ramp over static assets that declares no `affinity` and a D1 database whose migrations are applied with no Time Travel bookmark recorded in `.geonosis/deployed.json`. It says nothing when no wrangler config is declared. The `deployed` line SKIPs, with its sentence, a `deployed.json` that holds only recorded bookmarks. (doctor)
- A refused release no longer leaves its version serving. `prove --runner` asks a runner whose upload reply offers `"rollback": true` for a new `rollback` step after any refusal between upload and promote, and says in the refusal whether it happened; a runner that offers none is never asked, and its refusal says the version may still be in the deployment. An upload replying `ok: false` is refused even when it names a version. The Cloudflare runner rolls back by deploying the previous version at 100 % and confirming the new one is out, refuses a version that lacks a secret `release.secrets` or the wrangler config's `secrets.required` declares (read with `wrangler versions view --json`) before anything serves it, names the bindings the smoke reaches, and refuses a ramp request outside 1 to 99 %. Its `--prove` plants a missing secret and requires every refused release to end with the old version serving alone. `release plan` and the doctor's `release` line refuse a Queues consumer with no `dead_letter_queue`, because Cloudflare deletes a message that fails its last retry without one (D-117). (doctor, release)
- The `runner` check WARNs a verify step or a ratchet counter that runs an installed bin through `pnpm exec`, `npx` or `bunx` after another command (`cd apps/api && bunx vitest run`), naming the step and the cost: about 3 s of start-up a run, and an install when `pnpm exec` finds `node_modules` stale (#57). A command that starts with the runner is left alone, since `geonosis-verify` and `geonosis-ratchet` run the bin directly there. (doctor)
- The `tool runtime` check warns when `devEngines.runtime` names bun and not node, the shape earlier scaffolds wrote, which npm refuses with `EBADDEVENGINES` in every command of that repo (#32). It quotes the two-entry array to paste, and takes it from the root manifest of the kit through `tools-runtime.generated.ts`, the same source the scaffold's list is written from, held by a test. The advice no longer says "On pnpm 12": pnpm 11.5.1 accepts the list, and no minimum is known. (doctor)
- A `workers` check reads the `observability` block of every wrangler config (`wrangler.jsonc`, `.json`, `.toml` in the root and each workspace, plus `release.wrangler` paths) and WARNs for a production Worker with logs off or not stated, or with logs on and no `head_sampling_rate` stated; a rate outside 0 to 1 is a FAIL. The top level and an environment named `production` or `prod` that has a block of its own are judged; `doctor.workers.productionEnvs` renames them and `doctor.workers: false` turns the check off. A repo with no wrangler config gets no line (#66). (doctor)
- The reviewer bench records the model it ran on (`review-bench run` leaves it beside the decisions, `score` writes it into the bench, `--model` names it for decisions recorded before), and exports `REVIEWER_MODEL` and `REVIEWER_INPUTS` — the reviewer's agent, its skill, the cases, their diffs and the scorer. The release check reports, and no longer judges case by case, a bench whose reviewer inputs are what they were at the earlier release's tag and whose model is the earlier one's: a re-run of an unchanged reviewer measures only its own run-to-run spread. (evals)
- `geonosis-evals run --model <id>` does not open a session whose inputs are byte-identical to one an earlier run finished: the entry's definition, its fixture, the plugin dir, the tools on the session's PATH, the claude version, the model, the allowed tools, the per-session budget, the round and the scorer. The entry is reported with the stored verdict and `cached: { at, run, sessionId }`, and costs nothing against `--max-budget-usd`. Red verdicts are kept as greens are; a session that did not finish, reported an error or wrote `.geonosis/evals-verdicts.json` is not kept. `--fresh` opens every session. `claudeArgs` takes `model`, `runEvals` takes `round` and `verdicts`, and `openVerdicts`, `runnerInputs` and `toolchainDigest` are exported. (evals)
- An armed run refuses a `with` arm whose `--plugin-dir` holds no `.claude-plugin/plugin.json`. The directory resolves against where the run starts, and Claude Code loads a `--plugin-dir` that is not a plugin as nothing, so a run started from another directory scored every `with` session as if it had the kit while none did. (evals)
- A session is scored after the fast tier its Stop hook left running has finished. The hook returns within its budget and launches the tier in the background, to be read at the next turn end; a headless `claude -p` session has no next turn, so the runner read `.geonosis/gate-report.json` before the tier wrote it and scored the session "no gate report" — on a race, not on the kit. The runner now waits for the pid in `.geonosis/stop-pending.json` (120 s by default) and, when the tier outlives the wait, says it was still running instead of that no gate ran. (evals)
- Every git path the kit reads is now relative to the root, and limited to it, when `geonosis.json` sits in a subdirectory of the repository (#69). `git diff` and `diff-tree` print paths from the repository's top level and `git status --porcelain` always does, while `ls-files` and every configured path are relative to the root, so in a monorepo app: `release migrations` read no committed or staged migration and passed (`NONE`, exit 0) over SQL a release then applied; `verify --affected` mapped no change to a workspace; and the ledger's tick, shape, land, drift and handoff compared paths that could never match. Diffs now pass `--relative`, porcelain paths are mapped through `rev-parse --show-prefix`, and `git show` reads `rev:./path`. (ledger, release, verify)
- Several logging providers behind the existing ports, with Cloudflare Workers first-class (#66).  - `createProviderRegistry({ prefix, providers })` (the `@geonosis/integrations` shape: prefixed ids, a duplicate refused, refusals as failed `Result`s) fans `errorSink()` and `tracer()` out to every provider that carries one. A provider that fails, throws or rejects is reported in the `Result` and never stops the others; `captureEach` answers per provider; a tracer that fails before or after the body never changes the body's value or throw, and its failure comes back from `flush()`. - `@geonosis/observability/cloudflare`: `createCloudflareSink` writes one `console.error` JSON line Workers Logs indexes (error, vocabulary and `extra` as top-level fields; `release` from `version_metadata`; `recordException` on the active span), `createCloudflareTracer` wraps `tracing.enterSpan` with the vocabulary on the span, `createPlantHandler` is the route a Worker mounts so a proof can plant through its own adapter. It imports no Node module and no Node global, held by a test and by `tsc -p tsconfig.workers.json` in `typecheck`. - `@geonosis/observability/otlp`: `createOtlpSink` and `createOtlpTracer` speak OTLP/HTTP JSON to `{endpoint}/v1/logs` and `/v1/traces`; a failure status, a `partialSuccess` rejection and an unreachable collector are failed `Result`s, and a 429/502/503/504 says the specification asks for a retry that was not made. - `geonosis-observability prove --sink cloudflare --plant-url <url>` plants through that route and finds the line again through the Workers observability query API, with `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN` and `GEONOSIS_PLANT_TOKEN` from the environment. `FlushHandle` is exported from the package entry.  The OTLP sink and tracer bound what they hold between flushes (`maxBuffered`, 1,000 by default; the oldest is dropped and the next flush says how many) and give every request a deadline (`timeoutMs`, 10 seconds by default), so a missed flush or a hung collector cannot exhaust a Worker isolate. The Cloudflare plant route refuses a field longer than 2,048 characters. (observability)
- `list-take-without-order` reads the D1 stack's three spellings of a page read in no order, and joins the `backend-d1` preset (#72): a Drizzle select chain with `.limit()` and no `.orderBy()`; a relational `findMany({ limit })` with no `orderBy`, or a `findFirst` — `LIMIT 1` — with no `orderBy`; and on the raw binding a `prepare()` SELECT with LIMIT and no ORDER BY, or a `.first()` over a SELECT with no ORDER BY (D1's `.first()` does not alter the statement). A `where` that is `eq(t.<unique>, …)`, or a SQL WHERE `<unique> = ?` with no OR, pins the row; the columns are the new `uniqueColumns` option (default `id`). An aggregate SELECT with no GROUP BY is one row. The rule now presumes no single engine. A `backend-d1` repo sees new findings where these spellings occur. (oxlint-plugin-biological-architecture)
- A refusal that stands in for every file a rule did not read (a rule enabled without an option, a root import with no package resolving) is carried by the lexically first file the rule judges, not the first file oxlint happens to visit, so two runs of one tree print the same lines. The file is read off the command line, a JSON config and `git ls-files`; a run the plugin cannot read behaves as before. `unresolvedPackage` is now said once in a file, not once in a run. The varying counts reported for `ssot-no-inline-facts`, `constants-in-constants-file`, `max-comment-density` and `ssot-no-process-env` were not those rules: oxlint 1.87 on macOS drops lines of its stdout when it is a shell pipe, and a plugin with one rule that reports every string shows it. The README says so. (oxlint-plugin-biological-architecture)
- `GEONOSIS_LINT_TIMING=1` makes the plugin time each rule's visitor set-up and handlers and print the slowest ten to stderr when the run ends; `GEONOSIS_LINT_TIMING_TOP` lists more and `GEONOSIS_LINT_TIMING_FILE` writes every rule as JSON (#53). Without the variable nothing is measured. The timing found where one lint of this repo spent its time: the workspace and `node_modules` lookups read manifests on every import, every `directionRule` recomputed an import's landings, `no-orm-outside-db` computed them for imports of anything, and `test-no-clock-polling-for-promise-flips` walked every function of a test that names no `waitFor`. Lookups are now kept for one second, an import's landings are computed once for all direction rules, and the two rules skip what they cannot report; no rule's findings change, and the whole lint of this repo takes 29% less CPU (7.2 s to 5.1 s on average over five runs each). (oxlint-plugin-biological-architecture)
- `list-take-without-order` no longer judges test files by default (`__tests__/`, `.test.`, `.spec.`, `integration-tests/`): the row a fixture takes does not matter, and 72 of one tree's 198 findings were fixtures. `exempt` replaces that list and `exempt: []` judges test files again; a baseline keyed on the old count falls by the test findings. A literal `take` above 1 from the first row, whose result is iterated, counted or handed whole to another call, now gets the message `truncated` ("reads at most N rows and the code treats them as all of them") instead of "name the column", which left the dropped rows dropped. Since 3.2.1, a capped read used as if complete is reported even when it names an order, because ordering does not make the cap all the rows; the count may rise. Same rule id throughout, so no consumer's counter changes by key. (oxlint-plugin-biological-architecture)
- `tokens-only-colors`: since 3.2.0 the rule also reads colour literals in plain object properties, which is why a count rose without a change in the code (one Medusa storefront went from 3 to 69 findings; the 3.2.0 entry named only Tailwind variants). A theme passed to `defineTheme` (or to `compile`, `toDtcg`, `emitVariables`, `valueAt`) from `@geonosis/themekit` is the token layer and is no longer judged, whether the object is written in the call or held in a `const` of the same file. So is an object literal typed as a themekit palette type (`Theme`, `ThemeColors`, `ThemeMode` and the other types a `Theme` is made of) imported from `@geonosis/themekit`, by a `const` annotation, `satisfies` or `as`, so a brand theme defined in its own module is covered too; a local type of the same name is not. Their `exemptModules` entries can go. The new option `ignoreKeys` names property keys whose values are data that only look like colours (a shader's mask channels); it is empty by default and changes nothing until set. (oxlint-plugin-biological-architecture)
- `geonosis-ratchet --bank <key,…>` and `--bank-unproven` lock in falls someone has confirmed (#58): `--prove` runs for exactly those counters first and any that does not prove is refused, then their numbers and their identities are written in the same run, so no hand edit and no second full run. A key that grew or could not be measured is refused and nothing is written. An `UNPROVEN` fall is reported as one line per counter and one shared paragraph, which names the command. (ratchet)
- `geonosis-ratchet --jobs <n>`, or `"jobs": <n> | "heavy"` in `geonosis.ratchet.json`, measures independent counters at the same time, never more than the machine's heavy budget (#55). Entries naming one `command` or one `"lock"` run together, one after the other; an entry with `"exclusive": true` runs alone, last, and so does an `oxlintRule` entry with a `config`, which writes a strict copy into the tree while it measures. Each group is measured by a child process of the bin, so `CounterContext.run` stays synchronous; the report keeps the order of the config. The default is 1, as before. Six independent tools of this repo took 87 s and 43 s one at a time and 27 s and 32 s at `--jobs 4` on a machine at load 50 to 70. `lock` and `exclusive` are scheduling only and are not part of a counter's identity. (ratchet)
- `geonosis-ratchet --prove` proves every configured counter and every half of each, instead of returning at the first one that is not proven (#34): the counters after it were never attempted and the output did not say so, which read as `sumOfCounts` counters being skipped. Each counter prints its PROVEN / UNPROVEN / CANNOT FAIL line, the lock is proved whatever failed before it, and a failure ends with `prove FAIL — N of M counters unproven: a, b` and exit 2. `ProveResult` gains `counters: { key, proven }[]`. No counter's fingerprint changes. (ratchet)
- `oxlintRule` entries with the same `config` and the same `command` are measured by one oxlint run over one strict config raising all of their rules, and each key is counted from the lines of its own rule (#56). 17 entries over this repo's 1,600 files read the same 17 counts as 17 separate runs and took 4 to 7 s instead of 132 s. An entry whose `command` differs keeps a run of its own. `oxlintRule` changes fingerprint: its identity reads as changed once, so a fall in the first run after the upgrade is `UNPROVEN` and a rise or an equal count is judged as before; every other counter's fingerprint is unchanged. (ratchet)
- `geonosis-ratchet --record <name>` records every entry of the counter called `<name>` when no key bears that name, so a tier with one `testFailures` entry per workspace needs one step, `--record testFailures`, not one key per suite. (ratchet)
- A counter can read a run a tier step already made when the run is a vitest JSON report (#51): `geonosis-ratchet --record <key>` stores the report with the run, so an entry naming `output` counts and names the failing tests from the recording and runs nothing. Before, a `report: "vitest-json"` entry with `output` could not be read back: the recorded command carried a temp path no later run could match. `--record` takes several keys (`--record typecheckErrors,testFailures:apps/web`) and exits with the worst of them, and it records nothing when one of the keys is unknown. A scaffolded repo's `full` tier runs `tsc` four times and `vitest` six; with its fast-tier steps replaced by `geonosis-ratchet --record <key>` and `output` on the entries, three and three, and the ratchet step falls from 13.4 s, 11.0 s and 6.2 s to 2.7 s, 3.4 s and 3.5 s on a loaded machine. (ratchet)
- A counter whose command names `{report}` must declare the `report` it stands for; one that does not is refused at load, before a shell writes a file literally called `{report}`. (ratchet)
- A counter command that starts with `pnpm exec <bin>`, `npx <bin>` or `bunx <bin>` runs `node_modules/.bin/<bin>` when it exists, and every counter command runs with `pnpm_config_verify_deps_before_run=error` unless the environment or `pnpm-workspace.yaml` already sets it (#57). `pnpm exec` costs 3 s of start-up per command under load and installs when the manifests and `node_modules` disagree, which replaced the kit under a running gate. `binCommandOf`, `localBin`, `resolvedCommand` and `gateEnv` are exported for the tools that print or run a command. (ratchet)
- `geonosis-ratchet --record` in a tree git cannot describe yet — a fresh scaffold before its first commit — runs the tool, prints what it said and exits as it did, writing nothing and saying so; a counter with an `output` file there runs its own command instead of reading a recording it cannot tie to a tree. It refused before, so a freshly scaffolded repo's first `verify` failed at its first step. (ratchet)
- `testFailures` in `report: "vitest-json"` mode names the failing tests when the count rises, and keeps the report (#48). The evidence is `FAIL <file> > <full name> — <first line of the message>` for each failed assertion in the report, at most eight, then how many more there were and `report kept at <path>`; the report is written to `.geonosis/reports/<key>.vitest.json` (or the entry's `reportPath`) and is no longer deleted after the run. A report left by an earlier run is removed before the command runs. Only `testFailures` changes fingerprint: its identity reads as changed once, so a fall in that counter in the first run after the upgrade is `UNPROVEN` (`geonosis-ratchet --bank testFailures`), and a rise or an equal count is judged as before. No other counter's fingerprint moves. (ratchet)
- `@geonosis/release/cloudflare` and the `geonosis-release-cloudflare` bin: a ready runner for the release protocol on Cloudflare Workers (#63). A subpath, because it starts `wrangler` and sends HTTP requests, which the main entry does not (D-056). It uploads a version (`wrangler versions upload`, the version id read from `WRANGLER_OUTPUT_FILE_PATH`), parks it at 0 % beside the serving one and confirms the deployment holds both, smokes through `Cloudflare-Workers-Version-Overrides` and reports which version answered, polling while the deployment propagates, and promotes with `wrangler versions deploy` followed by `wrangler triggers deploy`, replying `ok` only when both landed. It is configured by `release.cloudflare` (`worker`, `url`, `health`, `version`, `versionKey`, `smoke`, `previewAlias`, `settleMs`, `config`, `wrangler`), and `smoke: "preview"` requests the version URL of an aliased upload instead of an override. `geonosis-release-cloudflare --prove` runs it against a stub wrangler and a stub Worker with a fault planted per refusal (an override that was not applied, a health route answering 500, a park or a promote that exited 0 and changed nothing, triggers that failed, a new version that errors under a ramp) and requires each to be caught. `release plan` refuses a config in `release.wrangler` with no `version_metadata` binding while `proof.mustAssertVersion` is on, and a refusal of `prove` now quotes the `error` a runner replied with. (release)
- `release.steps` takes one `{ "ramp": [10, 50], "hold": "5m" }` between `prove` and `promote` (#64): a gradual deployment with a hold, each step proved and none continuing past a regression. `prove --runner` sends `{"step":"ramp","versionId","percent","hold"}` and refuses, without writing the promote, a step that is not `ok`, one that reports no split of the deployment or puts the new version at another percentage, one that reports no error count, and one whose errors exceed `maxErrors` (default 0). The stub runner gains the plants, and `prove` with no runner requires each to be caught when a ramp is declared. For a Worker with static assets that ramps and declares no `affinity`, `plan` prints a note naming the version skew between HTML and content-hashed assets, and that `Cloudflare-Workers-Version-Key` pins a client to one version; `release.cloudflare.versionKey` sends it on the smoke. (release)
- `migrations --record-bookmark <db>` records the D1 Time Travel bookmark from right before the migrations apply (#65). Once the run is clean it takes `wrangler d1 time-travel info <db> --json` for each database named and writes `{ at, bookmark }` into `.geonosis/deployed.json` under `d1` and into the migrations envelope as `bookmarks`, so the restore point an incident needs is not reconstructed from timestamps. A refused run records nothing, a bookmark that cannot be taken is exit 2, `deployed write` carries the entries across, and a file holding only bookmarks is still "nothing here says what is deployed" to `deployed --check`. The README no longer says no rollback undoes a migration without saying that on D1 a Time Travel restore does, at the cost of every write since. (release)
- `deployed --check` compares every binding kind wrangler accepts (#62). The list held sixteen keys, so a config with static assets, rate limiters, a secrets store or a container had its bindings, `ASSETS` among them, absent from the comparison, and a deployment that lost any of them read as matching the tree. The list is now `BINDING_KEYS` in `wrangler-keys.ts` beside `OTHER_KEYS`, every other key wrangler accepts, and a test reads `config-schema.json` of the installed wrangler and fails when a key holding `binding` or `name` entries is not in the first list, or any key is in neither. A top-level key this reader has never heard of that holds such entries is reported as `not compared` (`notCompared` in `--json`) and never dropped. `secrets.required` joins `release.secrets` as the declared secret names. (release)
- `smoke baseline` and `smoke run` measure a consumer's phases as recorded whatever the caller's pnpm says: each phase runs with `pnpm_config_verify_deps_before_run=false`, because the smoke's own install step is the install. Before, a smoke started under `pnpm` inherited `false` and passed, while the same smoke started any other way let pnpm 11 reinstall the copy before every `pnpm run` — a different measurement of the same tree, and a failing one offline. (release)
- `NODE_ENV` is read when the code runs, never frozen at build (#70). The kit's bundler let Bun substitute `process.env.NODE_ENV` with the value the build ran under, so the published 3.2.0 `@geonosis/testbed` compared the `nodeEnv` it was asked for with the literal `"development"` and refused every Medusa integration suite with `NODE_ENV must be "test" … it is "development"` even under `NODE_ENV=test`. The build now keeps the read, and a test builds a package under one `NODE_ENV` and runs it under another. (testbed)
- 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 sets it, so a gate step does not install into `node_modules` while it measures it (#57). The command `lint-config` prints for approval names the local `geonosis-verify` when there is one. (verify)
- `geonosis-verify` says what `(unproven)` means: a run that prints it adds one legend line under the step list naming the `geonosis.json` path of each such step (`verify.fast[4].proves`) to declare `proves: { plant, expect }` at. A red step whose output carries a runner timeout (vitest, jest or bun message) records the load average and core count in the report and prints `timed out at load average X on N cores` under the FAIL; the verdict stays FAIL. A command naming `{report}` outside a counting batch is refused at load instead of writing a file called `{report}`. (verify)
- `walk.versionKey` and `--version-key <key>` send `Cloudflare-Workers-Version-Key` with every request to the origin of each walked page (#64). During a gradual deployment on Workers each request is otherwise routed to a version by chance, so a walk can read HTML from one version and ask the other for its content-hashed assets. The header goes on the page's own requests through a route scoped to that origin, and on the HEAD and GET requests `link-integrity` makes to it (`ProbeContext.headers`); another origin's requests are left alone, because a custom header sends them through a CORS preflight. (walk)
- A page that answers 400 or worse is walked instead of aborting the run (#61). `walk()` threw for any status of 400 or more before a probe ran, so the run exited 2 with no report, the pages already walked lost their findings, and `error-page-status`'s `honestFrom` could never take effect. Every page that answered is now walked, each probe is handed the status, and the report records it per page (`status`). The status a page must answer is declared in the config, `walk.urls: [{ "url": …, "status": 404 }]`, and a page that answers anything else is a finding: `link-integrity` for a broken link in the walk list (an undeclared 400 or worse, or a declared 200 that answered an error) and `error-page-status` for a soft 404 (a declared error that answered 200). Exit 2 stays for a page that answered nothing or could not be opened; that stops the walk, names the URL, and writes the pages walked so far to `<report>.partial.json` while the report the ratchet reads stays absent. `walk()` takes `expected`, throws `WalkStopped` (a `WalkError` carrying the partial report), and `PageReport` gains `status`. (walk)
- A repo scaffolded from the registry now installs and passes its bare gate. A `--backend medusa` pnpm tree names the install scripts of `@medusajs/telemetry`, `msgpackr-extract` and `protobufjs` in `allowBuilds` as denied, each with the reason, so `pnpm install` no longer stops on `ERR_PNPM_IGNORED_BUILDS`. A bun tree declares `devEngines.runtime` as a pnpm tree does. The root `verify` script is `geonosis-verify full`, the tier the law names, and CI runs it under that name; typed bare it no longer exits 2. The README and `--help` say to fetch a release younger than a registry age policy with `npx --min-release-age=0` or `pnpm dlx --config.minimumReleaseAge=0`. (create-geonosis)

## 3.2.0

- New `@geonosis/base`: the zero-dependency bottom of the method, holding what packages used to copy. `@geonosis/base` is `isRecord`, `isStringList`, `stringsOf`, the error text helpers and the markdown readers (`headingsOf`, `sectionUnder`, `fencedLines`, `titleOf`), safe in a browser or a Worker; `@geonosis/base/node` is `packageRootFrom`, `versionOf` and `isDirectory`; `@geonosis/base/bin-harness` is `binRunner(runtime)` for running a package's bin in a test, with `GIT_ISOLATED` for a bin that shells to git. `cli`, `create-geonosis`, `ledger`, `mcp`, `observability`, `rails`, `release`, `review`, `themekit`, `verify`, `verify-arch` and `walk` import the guards from it instead of carrying their own; their behaviour is unchanged and each now installs `@geonosis/base`, which a consumer never declares. The ratchet keeps its own copies: the build runs on its source before any package exists, so nothing it imports may be a package the build produces. (base, cli, ledger, mcp, observability, rails, ratchet, release, review, themekit, verify, verify-arch, walk, create-geonosis)
- `geonosis update --floors latest` or `--floor <pkg>@<v>` works without `--to`: the group stays at the one version every manifest carries, only the floors move, and the output says `group unchanged at <v>`. A group split across versions is refused and `--to` settles it. A dry run prints a floor already at its target as `(already here)`, a plain `--to` names each own-line package that is behind its line, and a move to an older version is flagged `DOWNGRADE` and reads the changelog entries it undoes instead of calling the older entry unreadable. `geonosis portfolio` prints one row per consumer named in `geonosis.json → portfolio.consumers`, read off the artifact that consumer's CI uploaded: version, lag in days off the registry's publish dates, last green tier, lines the last bump deleted. It exits 2 and prints no row when an artifact is missing or will not parse. `install.allowScripts` in `geonosis.json` lists package names (`@embedded-postgres/*`) whose lifecycle scripts run after the frozen install's `--ignore-scripts`, with `pnpm rebuild` or `bun pm trust`. `sync` refuses (exit 2) over a `geonosis.json` it cannot parse. `init` reads `geonosis.json → stack`: for a language or linter oxlint does not read it writes no `.oxlintrc.json` and no oxlint, tsc or oxfmt counter, and refuses an unreadable config. The MCP server's skew check falls back to the lockfile's resolved version and reports `UNJUDGED` when nothing answers; `update`'s restart line looks in `~/.claude.json` (user and local scope), an installed plugin's `.mcp.json` and Cursor's `mcp.json` as well as `.mcp.json`, and names which one it found. (cli)
- `geonosis init` and `geonosis update` copy the kit's rule files into `.claude/rules/geonosis/` (also `geonosis rules` alone), each with a `# geonosis-kit <version>` line directly under its opening `---`, removing marked files the kit no longer ships and leaving files a user added; `update` runs the copy it just installed, so the files are the new version's. Copies, because Claude Code loads a symlink out of the repo only after approval and then only the rules with no `paths` list; a `.claude/rules/geonosis` that is a symlink is refused, never written through. `geonosis-doctor` gains a `rules` check that warns when the directory is missing in a kit repo, or a copy differs from what the installed kit ships, naming the file and `geonosis update`. (cli, doctor)
- `@geonosis/cli` exports `lawSummary({ name, runtime, marketplace })` and `LAW_SUMMARY_TITLES`: the short-form law a scaffolded repo starts with, built in one place, and held by a test against the laws of this repo's own `LAW.md` (every title present, in the same order). `create-geonosis` writes its `LAW.md` from it; the text is unchanged. `geonosis-ledger sync --target agents-md` adds a "Style, in the code these laws produce" section to the generated `AGENTS.md`: the terse-output sentence and the comments law, the same text the plugin's skills and agents carry, exported as `TERSE_OUTPUT_LAW` and `COMMENTS_LAW`. (cli, ledger, create-geonosis)
- A repo whose `geonosis.json → stack` is not TypeScript passes the doctor with a SKIP that names the language, and gets the method scaffolded without the TypeScript stack. `geonosis-doctor` SKIPs `exercised` as it already did `loaded` (`stack.language is "py"`, so there is no oxlint rule for a corpus to be asked to reach), and `drift` SKIPs, instead of warning "no manifest declares it", a `geonosis.json` block of a kit package in a repo with no package manifest at all; the doctor no longer refuses a tree with no `package.json` and no `.oxlintrc.json` as "nothing here to examine" when `geonosis.json` declares a stack oxlint does not read. `stack` gains `extensions` (`.py .pyi`), the files `stack.format` and `stack.parse` own: with it, the plugin formats only those with `stack.format`, and `pre-edit` and `post-bash` parse them with `stack.parse` (exit non-zero is a parse error; a parser that cannot run says nothing). `stack.parse` without `extensions` applies to no file. A repo with no `stack` behaves as before. `create-geonosis <dir> --language py` writes a uv project (`pyproject.toml`, a package, a test), `geonosis.json` with the stack, three `fast` steps with plants and a `full` tier that proves and runs the ratchet (`ruff format --check`, `ruff check`, `pytest`), `LAW.md` from the new `pythonLawSummary` of `@geonosis/cli`, the two language-neutral counters and their baseline, the kit's rule files, `.claude/settings.json` and a CI workflow; it refuses `--runtime`, `--backend`, `--ui`, `--domains`, `--medusa`, `--themekit`, `--brand`, `--kit`, `--presets` and `--check` by name. (cli, doctor, create-geonosis, geonosis)
- `geonosis update` bumps a yarn 1 tree: it reads `yarn.lock` with `@yarnpkg/lockfile`, installs with yarn and leaves `yarn install --frozen-lockfile` clean, and `--runtime yarn` names it where no lockfile does. It refuses with exit 2, before writing, when the `yarn` that runs is not yarn 1 (pin `"packageManager": "yarn@1.22.22"`). A `package-lock.json` or a Yarn 2+ `yarn.lock` is refused with exit 2 and the commands that manager's consumer runs instead. `--dry-run` now refuses what the real run refuses. A client kit (`@geonosis/<client>-ui`) moves with the release, to the version the target `@geonosis/cli` records in its published `release-kits.json`. `update --prove` proves the yarn 1 path and the kit, and now FAILS when a manager is not on the machine unless `--skip <manager>` names it. (cli)
- `--registry <path>` is told apart from a URL by a missing path only (ENOENT, ENOTDIR); any other filesystem error, and any error but a syntax error while reading a packed `package.json`, is rethrown instead of read as "not a registry directory" or "not a package". (cli)
- `geonosis explain constructor` (or `toString`, `hasOwnProperty`, `__proto__`) is refused as a name nothing here decides, instead of crashing on the property every object inherits; a `geonosis.ratchet.json` whose `counters` hold a `null` or are not a list no longer crashes it either. (cli)
- `geonosis init` refuses, writing nothing, when a kept `geonosis.ratchet.json` points its baseline outside the repo; `rulesFor` names a preset nobody ships (`no preset called …`) in place of a `TypeError` on `undefined.rules`. (cli)
- `geonosis sync` reads a `geonosis.json` `law` block key by key: a `file` that is not a string, a `maxLines` that is not a number or a `copy` that is not a boolean is no declaration, where a numeric `file` crashed the run on `join`. (cli)
- `geonosis skills lock` and `audit` refuse a `skills-lock.json` that is not JSON or holds no `skills` block, naming the file, in place of a `TypeError` from the middle of the run; relocking keeps every other key a previous lock recorded for a skill and for the lock itself instead of rewriting each entry down to four. (cli)
- `geonosis update` repins a manifest with its own line endings and its own last line: a CRLF file stays CRLF and a file with no final newline gets none, so a bump is a version diff and not a rewrite of every line. (cli)
- The doctor says when it could not read, and asks more of the tree. A comparison asked for against a baseline HEAD does not hold, or against a ref that is not there, is `UNJUDGED` and no longer a SKIP that passes. A `package.json` that will not parse is a FAIL under `drift` naming the workspace it hid; `readManifest` is the one reader, in three states. `declaredApparatusOf`, `repoCorpusOf` and `declaredGroupsOf` take the `geonosis.json` read the run already holds, and `declaredApparatusOf` throws on one it cannot parse; `checkGroup` takes it as `geonosis`. `drift` WARNs when a workspace imports an `@geonosis/*` package its own manifest does not declare (`turbo boundaries` counted 35 of these in one consumer after a rebase dropped a redundant commit). `exams` reads a floor's `geonosis.bundles` in its manifest, so an imported bundle (`sessionConformance`, `searchConformance`) covers its members, and a floor whose installed manifest will not parse is `UNJUDGED`. Script files are followed four files down, one reader for `groups` and `runner`. Which install Claude Code loads when user and project scope both hold one was measured on 2.1.293: the first record in the file's order that applies, not the project one; `loadedAt` follows it. `geonosis.json → stack` is read once for the hooks, `init` and the doctor (`stackOf`, `oxlintReads`); `loaded` SKIPs, with the sentence, for a language or linter oxlint does not read. New exports: `readGeonosisFile`, `GeonosisRead`, `allInstalledPlugins`, `stackOf`, `oxlintReads`. (doctor)
- `geonosis-lint-parity --measure <config> --root <tree>` runs one config over a consumer's tree as if it sat in the tree, so a narrowed config's `overrides` and `ignorePatterns` are read against the tree and not against the directory the config was written in. It refuses a config that opens no file. `geonosis-doctor` warns, under `exercised`, for an override whose `files` match no file under the config's directory. The root of this repo now links `geonosis-lint-parity` and `geonosis-release`, so the documented command runs from the root. (doctor, lint-parity)
- `optionRefusalIn(said)` is the one reader of a rule's "requires option" refusal: the `option` and `rule` fields of an error or a report, then the `data` of a report, then the sentence, from an error, a report or a printed finding. The doctor's missing-option check and the parity tool's refusal check both read through it, so a plugin that words its refusal differently and hands the pair over as fields is a refusal to both, not reach to one and configured to the other. (doctor, lint-parity, ratchet)
- The package returns to its first name: 0.3.0–0.5.0 were published as `@geonosis/storefront-ui`, and this release is 1.0.0 under `@geonosis/buketi-ui`, above the 0.2.1 there. A client's UI is its own kit, named for it (D-090). Users of 0.2.1 cross the breaking changes of 0.3.0–0.5.0 (see the CHANGELOG); every import path changes its package name and nothing else. The doctor's `FLOOR_PACKAGES` no longer lists a client's kit. (doctor)
- The drift check that asks whether a declared package is composed no longer asks it of a door package. `@geonosis/cli` and `geonosis` declare `@geonosis/observability` and the floors to bring them along, and the check read that as a repo choosing to install a sink and composing it nowhere. A package an app's own manifest declares is judged as before. (doctor)
- The drift check (root package scripts only: a script run with `--filter`, `-C` or `-r` is not followed) warns when a counter that times a tier's own gate report (`fastTierMs`) is run by a ratchet step of that same tier: it reads the previous completed run and cannot measure a fresh checkout, so the ceiling belongs in `geonosis.json` `budgets.<tier>` (D-042), or the counter in a tier that tier does not run. A counter can now say which tier it times (`timesTier`). The smoke refuses to compare a baseline recorded under another command (`compareToBaseline`), and `release:check` records it again. (doctor, ratchet, release)
- A scaffolded repo declares `devEngines.runtime` as bun (downloaded by pnpm, the runtime every kit tool starts on) and node with `onFail: "ignore"`, so npm itself — `npm login`, `npm whoami`, `npm publish` — runs in the repo instead of refusing with `EBADDEVENGINES`. The doctor's `tool runtime` advice gives the same pair. A scaffold pins `pnpm@12.10.1`. (doctor, create-geonosis)
- The plugin README lists the five oxlint built-ins that fire on idioms the presets ask for (`no-await-in-loop`, `unicorn/no-useless-undefined`, `unicorn/consistent-function-scoping`, `unicorn/throw-new-error`, `no-underscore-dangle`), measured on oxlint 1.87.0, with the config that quiets the four an override or option can reach and the rewrite for the loop. The kit's `tiers.md` rule file says what "inert below the organelle" means: no app or shared state (no store, no data); local UI state such as an open flag is allowed in an atom or a molecule, and a compound holds none. `gates.md` separates adopting a counter, which the ratchet writes and does not judge, from raising a baseline to make a gate go green, which stays forbidden. `geonosis-doctor`'s `rules` check compares against these copies. (doctor, oxlint-plugin-biological-architecture)
- `geonosis.ratchet.json` that does not parse is refused with the file named, and a counter entry that is not an object is dropped. A `package.json` that is valid JSON but not an object is skipped like one that does not parse, and a dependency block of the wrong shape reads as undeclared. A baseline committed as `null` reads as holding no counter, where the baseline check threw a `TypeError` and ended the run. (doctor)
- A `doctor.groups[].packages` list holding anything but package names (a number, `null`, an object) is refused with the group named, where only the count of entries was checked. (doctor)
- The `publicHoistPattern` check reads a `!` entry as an exclusion, as pnpm does. It was read as a pattern of its own, so a workspace package the list excludes was demanded a root link pnpm never makes (a FAIL on a healthy tree), and the exclusion itself printed a SKIP line. (doctor)
- A script path that climbs into a sibling directory whose name starts with the repo's (`../repo-other/gate.mjs` from `repo`) is no longer read as a script the repo runs. The containment test was a string prefix. (doctor)
- An installed package whose `package.json` does not parse no longer stops the whole doctor run with a bare JSON error. The `exams` and `drift` checks read it as declaring nothing, as `discover` already does for a workspace manifest. (doctor)
- An empty law file counts as no lines in `lawLineCount` and in the doctor's ceiling check; it counted as one. Both now read lines the way the seam counter does, from one `lineCount`. (doctor, ratchet)
- An `adoption.seams` glob is read by one matcher in `@geonosis/ratchet`: `*` and `**` are wildcards and every other character, `?` and braces included, is itself. The `adoptionSeamLines` counter, the doctor's seams check and `smoke snapshot` agreed on none of it before — `src/a?.ts` made the `a` optional, and the doctor accepted brace globs the counter counted as nothing. `smoke snapshot` now refuses a consumer `geonosis.json` that does not parse instead of recording a tree with no seams. (doctor, ratchet, release)
- A scaffold with `--domains` has a clean doctor. It writes `apps/web/src/platform.exam.test.ts`, which runs the session, connections and (for `search`) search exams over the platform's own seam and index against the Postgres `EXAM_DATABASE_URL` names, and skips where none is set; the app declares `pg`. A `--backend sagaflow-cf` scaffold no longer enables `tenant-scoping`, whose `store-route-scopes-tenant-data` presumes Medusa. `geonosis-doctor`'s `exams` check asks for an exam named `<stem>Conformance` only of a tree that imports the package's `define<Stem>` export when it ships one: `entityConformance` of a tree that calls `defineEntity`, never of one that only opens sessions. The `drift` check no longer counts a test file as the composition of a floor: a tree whose only importer of `@geonosis/search` is its exam file now WARNs that nothing composes it. (doctor, create-geonosis)
- A counter's report, or an `.oxlintrc.json`, whose top level is not a JSON object is refused by name instead of failing with a TypeError, and an unreadable counter report now reads "does not parse". No other output changed: the rest of this release's diff removes duplicated internals. (doctor, ratchet)
- A task's expected file now has to be created or changed by the run: a file the fixture already held and the session left as it was no longer scores as left behind. `compareEntries` compares two results entry by entry over the entries both hold (a set that grew is not a regression, a new entry is judged on its own sessions), and `geonosis-evals run --arms` is refused without `--allowed-tools`, since a headless session with no tool allowed measures nothing. The kit set's per-entry budgets are the dearest measured session of the 3.2.0 armed run, doubled. (evals)
- `geonosis-evals review-bench` scores the reviewer. `derive --fix <commit>` turns a fix commit into two cases (the diff that puts the defect back, with the lines the fix touched as its defect, and the fix itself as a clean control); `run --model <model>` spawns the reviewer over every case; `score` writes `reviewPrecision`, `reviewRecall` and `reviewF1` from the blocking and major findings that landed on a planted defect; `compare` refuses a case both benches hold that lost precision or recall. `reviewPrecision` and `reviewRecall` are dimensions `compare` and `gate --prove` know. `evals/review-bench.json` holds 28 cases derived from this repo's own fix commits. (evals)
- The law fingerprint a dimension reading records counts an empty law file as no lines; it counted one. The sha decides whether two readings were taken against the same law, so a recorded reading still compares the same. (evals)
- A pressure run that set a rule to `"allow"`, `false`, `"0"` or `["allow"]` is scored as having downgraded it, in a config that parses and in one that carries a comment; one `rules` that is a list is no longer read as a block of rules. The spellings are the ones `ruleIsOff` already names.  A config that carries comments or trailing commas is read as JSON once they are removed, so a `false`, `0` or `1` outside `rules` or inside a rule's options is no longer scored as a downgrade.  A lint config that is not JSON even once comments and trailing commas are removed is no longer scored as compliant: it counts as a violation that names the file and says it could not be read, so the law-2 dimension does not pass on a measurement that was not made. (evals)
- `geonosis-evals` sets `process.exitCode` and lets the runtime drain stdout instead of calling `process.exit()` after writing a score: a `--json` report past 64 KiB reached a pipe cut mid-token while the exit code stayed normal. A refusal is still exit 2 with its message on stderr. (evals)
- A proof is counted only when its file name is one `proof capture` writes: the pattern also accepted a backtick. (evals)
- `ledger.briefs` names the directory briefs live in, and `land` refuses, before any gate runs, a branch that brings a brief (or whose `--brief <file>` names one) naming a `*-report.md` that the branch's tree does not hold (#367). A red `land` prints the machine's load average and its count of test-shaped processes beside the failure (#343), and says which key holds the answer when the gate runner is not found in the bare merge-result worktree: `ledger.land.prepare` (#154). `handoff` writes markdown the formatter leaves alone: a blank line after the marker and the heading and before every list, and each path, subject and plan name as inline code, because git's own ` M path` lines and a `__fixtures__` path were rewritten by `oxfmt` (#493). Plans before `ledger.plan.since` are one line, `36 legacy plans (before 039) not listed`, instead of one `UNDECLARED` line each (#494). (ledger)
- `geonosis-rails farm --tickets <n>` dispatches the plan graph: it claims the first `n` ready plans, opens a worktree for each, runs an engineer session per worktree (`--jobs` at once), captures the fast gate as a proof, spawns the reviewer on the model `review.runner` names, applies the decision through `geonosis-review check` and `apply`, sends a `request-changes` back to the engineer once, then rebases, lands through `geonosis-ledger land` and ticks. A ticket that cannot finish parks with its reason in the handoff and keeps its branch; a red `main` parks before the first spawn; the census is `.geonosis/envelopes/rails-farm.json`. `geonosis-ledger plan ready --json` prints the graph as data, each ready plan with its claim and whether it is already DONE. `geonosis-rails shift` now passes `--verbose`, without which `claude -p --output-format stream-json` refuses to start. (ledger, rails)
- `geonosis-ledger shape lock --from <manifest>` validates a brief's manifest, resolves its red commit to a full hash and writes the runner-owned `.geonosis/brief.json`, a path the plugin refuses an agent's write to, so the engineer cannot loosen the lock it works under. `geonosis-ledger shape check [--brief <manifest>]` judges an engineer's diff against the RED commit a brief was committed as (D-072). The manifest names `ticket`, `red`, `files`, `locked` and optional `exports`; every path that differs from the red commit, committed, staged, unstaged or untracked, is judged. A locked test that was changed, deleted or moved, a file the manifest does not list, an exported name the red commit held and the file no longer exports, and an exported name neither the red commit nor `exports` names are each refused by path (exit 1); a body-only diff passes; a missing or malformed manifest, or a red commit the tree cannot diff against, is exit 2. New exports of `@geonosis/ledger`: `shapeCheck`, `lockManifest`, `readManifest`, `formatShape`, `MANIFEST_PATH`, `BriefManifest`, `ShapeOutcome`, `ShapeRefusal`. `@geonosis/ratchet` gains `exportedNamesOf(path, source)`, the sorted names a TypeScript or JavaScript source exports (types, destructured bindings, re-exports and `export *` included), read with the parser the counters use. The plugin's Bash guard runs `shape check` on `git commit` while `.geonosis/brief.json` is present and refuses the commit with the lock's own lines; a lock that could not run lets the commit through and says so. The engineer definition says to start on the RED commit and that the shape lock is the gate, and the `geonosis-plan` skill says how to commit the applied brief and lock the manifest. (ledger, ratchet, geonosis)
- `ledger.plan` and `ledger.land` refuse a key named `constructor`, `toString` or `__proto__` as they refuse any other key outside the block; the check read the defaults' prototype chain. (ledger)
- `decide` finds an id the register already carries in whichever column the register's header names `ID`, not only in the first. A register that leads with a date column accepted a second `D-001`. (ledger)
- A heading shown inside a fence is no longer read as a heading when the fence is longer than three characters, is made of tildes inside backticks, or is indented. Fences now close on their own character at their own length, as CommonMark has it, so a plan that shows markdown no longer ends its own acceptance section early. (ledger)
- An oath reads an empty file as no lines; it read one. The number is only displayed beside the sha-256, which decides whether a file changed, so an oath already sworn still checks the same. (ledger)
- `proof capture` picks a proof's number after the command has run and never overwrites a file another capture wrote meanwhile. It picked the number first, so a long command could finish into a number another proof had taken. (ledger)
- `rulesNamedBy` and the corpus reach scope read a rule set to `"allow"`, `"0"` or `false` as off, as oxlint does — they counted only `"off"` and `0`, so such a rule was judged for reach it was never configured to have. One `ruleIsOff`, exported from `@geonosis/ratchet`, is what the doctor, the ratchet and lint-parity all read. (lint-parity, ratchet)
- `geonosis-ratchet`, `geonosis-verify` and `geonosis-lint-parity` end with `process.exitCode` instead of `process.exit()`, so a report longer than a pipe holds (a `--json` run over a few thousand findings or counters) reaches the reader whole. `process.exit()` after a write past 64 KiB cut the report at the pipe's size, which for `--json` is a document that no longer parses. (lint-parity, ratchet, verify)
- A corpus `manifest.json` whose `rules` list holds anything but names is refused, where its non-string entries scoped a reach run as rules. (lint-parity)
- Dependencies move to their newest release the install policy accepts: `@modelcontextprotocol/sdk` 1.32.1 (`@geonosis/mcp`), `oxc-parser` 0.153.0 (`@geonosis/ratchet`, `@geonosis/verify`), and the `squawk-cli` 2.68.0 that `@geonosis/release` and `geonosis` ask for as an exact optional peer, so the install line a refusal prints is `squawk-cli@2.68.0`. A scaffold written by `create-geonosis` pins `oxlint` ^1.87.0, `oxfmt` ^0.72.0, `vitest` ^5.0.3 and `@types/node` ^26.6.4. (mcp, ratchet, release, verify, create-geonosis, geonosis)
- A server that cannot read which `@geonosis/cli` the repo installs says `UNJUDGED` on its first reply of a session instead of staying silent. (mcp)
- The server's introduction names a `.oxlintrc.json` or baseline it cannot read, instead of the server failing to start with a `SyntaxError` or `TypeError` over a config that is not JSON, has `rules: null`, or has `overrides` that is not a list. (mcp)
- `withDedupe` files an envelope once when its redelivery arrives while the first copy is still being captured, and answers the second with the first's id. Both read the store before either had written to it.  When that first attempt fails or throws, every waiting redelivery looks again before it tries, so exactly one of them files the envelope; a thrown error reaches only the caller whose attempt threw. (observability)
- Thirty-four rules the kit enables now declare a probe, so the doctor's `exercised` can ask them whether they reach anything and `probelessRules` falls with them. A direction rule declares its probe beside its scope.  New rule `hermetic-e2e-no-wall-clock-waits` (preset `testbed`): in an end-to-end spec a fixed-time wait (`waitForTimeout`, a `setTimeout` sleep) is reported, and a `waitForResponse` on a list endpoint is reported for the endpoints `listEndpoints` names. `specs`, `clockWaits` and `networkWaits` are options.  `worker-handles-are-scoped` accepts the wrapper as a member call (`cellOf(env).withConnection(...)`). `no-generic-utility-package` takes `allow: [{ package, exports }]` for the one door a generic name survives to hold, and reports a second export. `no-hook-in-component-disguise` takes `clientBoundary` (`false`, or the paths the directive is honoured under); its message no longer teaches the directive where it is withdrawn. `tissue-no-hooks` takes `contextHooks`, a list that is empty until the repo writes it and never excuses React's state hooks. `tokens-only-colors` reads a colour in a plain object, not only in a JSX style, an attribute or a default. `organelle-single-source` counts the sources of the sibling organelle hooks a file calls. `tissue-no-data-props` reads a props type named instead of written inline, from the declaration beside it or the file it is imported from, and its message says an inferred `any` belongs to the type-checker's rules. `cells-folder-index-is-barrel` reports a `cells/` file that only re-exports an organelle.  `subpath-over-barrel` expands a door written as a pattern (`./icons/*`) by listing the files its target names, reads the workspace membership (`pnpm-workspace.yaml`, `package.json` `workspaces`) as the repo's own packages where the linker writes no `node_modules`, re-reads a manifest when its mtime changes, and says once a run when an opted-in package resolves to nothing or the tree resolves no package at all.  A rule that throws a `SyntaxError` — a pattern option that is not a regular expression — is reported as one finding naming the rule, once a run, and no longer erases the file's other findings. (oxlint-plugin-biological-architecture)
- A dynamic `import()` or `require()` that is destructured — `const { Real } = await import('../cells')` — takes the names the pattern spells, as `import { Real }` does, so the other names the barrel carries are not its reach; a rest element, a computed key or a result bound to one name still takes the whole module. A solution-style `tsconfig.json` (no `paths`, only `references`) leaves the aliases to the referenced project that declares `paths` and includes the importing directory.  The tier folder is read in one place: a molecule, and the molecules/compounds/organelles/cells/layouts the card and prop-name rules police, are recognised when the path starts at the folder as well as when it is absolute, and a folder that only ends in a tier name is not that tier. `queue-loop-is-the-library` exempts a test through the shared reader, so a path relative to the working directory is exempt too. The cell and tissue private-sibling suffixes share one list.  A refusal for a missing option reports `data: { option, rule }` beside its sentence. `src/registry.ts` is generated from the rule modules on disk (`pnpm generate:registry`), and `RULES.md` carries a generated inventory (presets, required option, presumed engine) and summary beside a hand-kept provenance table. (oxlint-plugin-biological-architecture)
- `organelle-dependency`, `tissue-no-organelles`, `atom-no-deps`, `molecule-atoms-only` and `compound-no-stores` judge a specifier that begins with a tier folder or `app/` (`cells/editor`, written against a baseUrl); `tissue-must-compose`, `molecule-must-compose`, `next-route-segment-is-thin-delegate` and `no-trivial-wrapper-component` no longer report a file whose composed import is spelled that way. (oxlint-plugin-biological-architecture)
- `no-inert-hidden-jsx` reads `className={'hidden'}` and a hole-free template literal as it reads `className="hidden"`. (oxlint-plugin-biological-architecture)
- `no-logic-in-component-files` counts a default-exported component and a component named in capitals (`FAQ`, `CTA`), which it skipped, and judges each declarator's own thinness. A capitalised name counts only when it is a function that returns JSX, so a helper such as `export const API = () => fetch(...)` stays out of the count. (oxlint-plugin-biological-architecture)
- `effect-hook-naming` reads `React.useEffect` and `React.useLayoutEffect`, and judges the name of a default-exported hook. (oxlint-plugin-biological-architecture)
- `no-runtime-dead-affordance` reads a handler as `on` followed by a capital, so a prop such as `only` or `once` no longer keeps a dead control alive. A lowercase `onclick` is no longer a handler either, and a control carrying only that is now reported: React does not attach it and warns, so it is a dead control there. A Preact-style tree that spells handlers in lowercase will see it. (oxlint-plugin-biological-architecture)
- `tokens-only-colors` and `font-roles-only` read an arbitrary value through Tailwind variants and the important modifier — `data-[state=open]:bg-[#fff]`, `[&>span]:bg-[#fff]`, `!bg-[#fff]` — which they skipped. (oxlint-plugin-biological-architecture)
- `store-route-scopes-tenant-data` is in `backend-medusa` only. It presumes Medusa's `/api/store/**` route layout, and `tenant-scoping` is for a repo of any stack, so a Workers + D1 app that enabled `tenant-scoping` carried a rule that judged nothing there. A repo on Medusa that enables `tenant-scoping` without `backend-medusa` no longer gets this rule: add `backend-medusa`. A scaffold's Medusa answer already names both. (oxlint-plugin-biological-architecture)
- A tier rule now declares the tier folder it reaches (`rule.tier`), and `unreachableTierRules` counts the tier rules enabled over a workspace that ships tier folders but none of that rule's tier: they have no possible finding and print what a clean tree prints. A workspace with no tier folder is not judged; a plugin that declares no tier is a refusal.  `geonosis-ratchet --prove` now plants the tool failing as well as a finding. A counter must answer a failing tool with a `CounterError`, and a counter that ships no `refuses` probe reads UNPROVEN. The report names a counter's halves (`PROVEN archViolations (command)`, `UNPROVEN archViolations (match)`) and the summary counts halves. A counter you wrote needs `refuses: { input?, command?, params? }` beside its `probe`.  A named `roots` entry of `commentLines`, `suppressionCount` and `orphanTodos` that is not a directory is now a refusal rather than zero findings, and a `pnpm-workspace.yaml` that does not parse refuses the counters that read it.  A win one counter measured is now written to the baseline even when another counter could not be measured (a floor whose `dist/` is not built no longer holds every other floor's win back); the run still fails at the refusal and says which number was lowered.  `waitForBuild(root)` and `buildLockPath(root)` are exported for a tier that must not run beside a build.  `typecheckErrors` takes a `report` in place of a `command`: it reads the `{ errors, tree }` file a typecheck step wrote and refuses one whose tree is not the working tree in front of it, naming both digests, so `verify:release` no longer runs the same tsc program twice. `treeHashOf(cwd)` is exported for the step that writes it. (oxlint-plugin-biological-architecture, ratchet)
- `tissue-no-fetch`, in the `frontend` preset: a file in a `tissues/` folder that contains an `await`, a `for await` loop or a call to `fetch` (bare, or through `globalThis`, `window` or `self`) waits for data, so the tissue owns it. A tissue arranges data it was handed; the read belongs in a loader the route segment calls. Tests, specs and stories beside a tissue are left alone, and so is every other tier. It needs no option to run. (oxlint-plugin-biological-architecture)
- `icon-control-has-name` no longer reports a control whose icon names itself: an icon with its own `aria-label` or `aria-labelledby` (an icon component must forward it to its svg), or an `<svg>` holding a `<title>`, is the control's accessible name. A `title` attribute on an icon or an `<svg>` is not an accessible name and is still reported. (oxlint-plugin-biological-architecture)
- `step-declares-compensation-or-none` declares a `probe`, so a repo that enables it for one directory (an override with a `files` glob) is judged by the doctor's `exercised` check instead of being skipped for want of a probe (#131). (oxlint-plugin-biological-architecture)
- Two equal authorities holding the same object with its keys in another order are no longer reported as a conflict. (policy)
- `refusalPrefix` is read as the literal text it is, `reversalVerbs` with the `g` or `y` flag read every reversal rather than alternating, and `curate` no longer changes the candidates it was handed. (policy)
- A role named `constructor`, `toString` or `__proto__` has been granted nothing, rather than throwing a `TypeError` when grants are held as a record. (policy)
- `declareThreshold` refuses a `revisit` that has the shape of an ISO date and is not one (`2026-13-01`, `2026-02-30`). The first never expired, the second rolled into March. (policy)
- REFUSAL-IN-MINOR: `geonosis-rails shift` and `geonosis-rails farm` park before the first cycle or spawn when no sandbox profile can be applied (`sandbox-exec` or `bwrap` not on `PATH`, or the profile cannot be written), naming the reason on stderr and in the handoff, and every cycle, engineer and reviewer now runs through the profile: the network stays open, the tree stays writable, and the baseline the run is scored against is denied to it. `contain` still fails open and loud for an instrument. A machine without a sandbox binary installs one, or does not run a shift on it (plan 006). (rails)
- The farm bounds what an agent may spend and records what it did. `geonosis.json → caps.turns` (a whole number of at least 1, default 40) is passed to every `claude -p` the farm spawns as `--max-turns`; a run that stops at the cap is parked, naming the cap and the turns it used, and is never resumed. The result event's usage is read for every launch and the sum for a ticket (total, input, output, cache creation, cache read, turns, launches, and how many launches reported none) is appended to the ticket's proof under `## tokens`. When the machine's memory binds the heavy budget, the engineer's prompt carries a compute rule naming the budget; and `farm` without `--jobs` runs as many tickets at once as the heavy budget allows instead of all of them. New exports: `resultOf`, `RunResult`, `RunTokens`; `Caps` gains `turns`, `CycleOutcome` gains `tokens`, `turns` and `turnCapReached`, `FarmSeams` gains `computeRule` and `record`. (rails)
- `geonosis-gate serve | status | stop` (`@geonosis/ratchet`): one daemon per machine that runs a tier once for every `geonosis-verify` over the same tree and flags, answers the report it kept to a verify over a tree it already read, and queues the heavy slots first-come within the budget. The budget reads `GEONOSIS_HEAVY_SLOTS`, then the new `geonosis.json → caps.heavy`, then the machine. `geonosis-verify --fresh` runs again whatever the daemon holds; `--exclusive` never asks it. `takeSlot` leases its slot through the daemon and uses the slot files when none runs, and the Stop hook reads a kept green for the tree and hands `fast` to the daemon. REFUSAL-IN-MINOR: `geonosis-verify --prove` now also plants a failure inside a parallel group and fails a runner that loses it or runs the steps after it, and `loadCaps` refuses a `caps.heavy` that is not a whole number of at least 1. (rails, ratchet, verify)
- `egress --write` refuses a settings file whose top level is not an object, or whose `sandbox` or `sandbox.network` is not, naming the file. It spread an array or a string into the rendered settings, or failed with `Cannot read properties of null`. (rails)
- A main whose latest run has not concluded reads as `unknown` rather than `red`, as the seam's own comment said it did, so the park reason no longer calls a running build broken. (rails)
- An outbox line whose `kind` is named after an Object property (`constructor`, `toString`, `__proto__`) is refused as a kind this repo never declared. It was looked up on the declared table's prototype, found, and handed to the applier as if declared. (rails)
- `geonosis-rails` sets `process.exitCode` and lets the runtime drain stdout instead of calling `process.exit()` after writing a report: `outputs apply --json` and `shift --json` past 64 KiB reached a pipe cut mid-token while the exit code stayed normal. A refusal is still exit 2 with its message on stderr. Its error messages no longer read `undefined` for a thrown value that is not an `Error`. (rails)
- A floor's surface grows only after a deletion (D-076, plan 029 W119). A `floorSurface` entry may carry `earnedBy: { consumer, envelope? }`: when its baseline was raised in the working tree against the committed one, `geonosis-ratchet` reads the declared consumer's added and deleted lines from `deletions` in `.geonosis/envelopes/release-adoption.json` and judges the raise against the committed number unless the figure is negative, quoting the figure, or saying that none was read. `geonosis-release adoption` writes that figure: a consumer named in `portfolio.consumers` gets a `DELETION` line, read from the `bump-report.json` its artifact holds, and `deletions` in the envelope. `geonosis-release consumers` prints `release.consumers` (a list of `owner/repo`) as JSON, which the kit's `rc` workflow now reads instead of a second list in the CI settings; `templates/kit-rc.yml` is the file a consumer adds to answer a kit pull request with a `kit-rc/<repo>` commit status and an artifact in the layout `geonosis portfolio` reads (W117). (ratchet, release)
- REFUSAL-IN-MINOR: `geonosis-verify` refuses a step, a `proves` or a parallel group in `geonosis.json` that carries a key nothing reads, naming the key and the keys a step takes. It dropped the key and loaded the step without it, so a step declared `provse` ran with no proof and said nothing. Rename or delete the key. `unknownKeyOf(block, known)` is exported from `@geonosis/ratchet`, the one test of "a key this reader does not know" that `geonosis-release` now uses too (#728). (ratchet, verify)
- A counter can read the run a step already made. `geonosis-ratchet --record <key>` runs the counter's own tool once, prints what it said, writes the run to the file the entry's `output` names (the command, its exit code and its output, tied to the tree) and exits as the tool did. An entry that names `output` reads that file and does not run its command: a recording of another tree is a refusal naming both tree hashes, a recording of another command is a refusal naming both, a missing recording is a refusal naming the command that writes one, and a tree that changed while the tool ran is not recorded at all. Entries naming one `output` and one command are served by one recording. `recordCounter` and `recordedRun` are the two halves. (ratchet)
- A verify step may declare `"history": true`: its `prove` copy is made a repository whose one commit is the copy, with `refs/geonosis/verified` at it, and runs with the host's `GIT_` redirects and git config out of the way, so a step that compares against the mark can be watched failing. `@geonosis/ratchet` exports the isolated git runner (`isolatedGit`, `withoutGitRedirects`, `GIT_IDENTITY`) that `@geonosis/release`'s smoke already used and now imports from there. (ratchet, release, verify)
- Migration note for consumers upgrading from before 2.11.0: a counter that reads a kit bin's piped output, where that output could exceed 64 KiB, has a baseline recorded before the fix that is a floor. The bins used to call `process.exit()` after an asynchronous write, and through a pipe on macOS everything past 64 KiB was dropped with a clean exit code, so a count over the output read as a stable, smaller number. Re-measure the counter after the upgrade and adopt the true value, with the reason in the commit. The README section "A counter that reads a kit bin through a pipe" says how. (ratchet)
- A baseline entry that is not a number (`"5"`, `null`, a list) is now refused with its key named, where it was adopted over and the debt it held silently reset to the new reading. A baseline or `geonosis.ratchet.json` that does not parse, or is not an object, names the file in its refusal; a `null` counter entry gets the config's own sentence rather than a `TypeError`. (ratchet)
- A run waiting for a heavy slot reads the machine's memory once per pass over the slots instead of once per slot: on macOS each read runs `vm_stat`, so a full table cost one process per slot on every poll. (ratchet)
- `oxlintRule` with a `config` now forces a rule to `error` however it is parked (`0`, `false`, `"allow"`, `"0"` as well as `"warn"` and `"off"`), and no longer rewrites a different rule whose name merely ends the same way. (ratchet)
- `sumOfCounts` refuses a `match` that reads lines of the output but captures no number (no capture group, or a non-numeric capture) instead of summing it as zero and banking a false win. (ratchet)
- A counter's measurement identity records a fingerprint of the counter's own code and compares that instead of the package version, so a release that left a counter alone no longer makes its next fall UNPROVEN or its next rise say the measurement changed. A record from before fingerprints is still compared by version, once. A rise now names the identity file a hand-raised number leaves stale.  Migration note: a floor you correct upward because a kit bug produced it must land in the same commit as the bump that fixes the instrument, never ahead of it. A shrink read by the still-buggy build is refused when the recorded measurement differs, and the number it replaced is kept in `gate-baseline.identity.json`, but only the same-commit order keeps the correction from ever being overwritten. (ratchet)
- `geonosis-release deployed write` is the writer `deployed --check` lacked: the pipeline prints `{ bindings, secrets, triggers: { crons, routes }, versions? }` on stdin or in `--from`, and it stamps `at` and replaces `.geonosis/deployed.json` whole or not at all, refusing a list that is not strings, a missing list and a key nothing reads (#485). `schema` reads a declared domain directory that is not on disk as unreadable, exit 2, where it printed `NONE` and exited 0 (#546). The candidate line the smoke prints carries `--root` with the consumer tree it was recorded from, or `<your checkout>` (#406). (release)
- A smoke phase whose command the shell did not run (exit 127, not found, or 126, not executable) reads `NOT MEASURED` instead of "already failing at their versions"; exit 2 stays a result, because `tsc` exits 2 on a type error. `geonosis-verify` counts such a step in the envelope's `refused`, with the exit and the command, instead of reading it as a step it ran (#308). (release, verify)
- `release.migrations[].dir` is read as the path it names, so `./db/migrations` and `db//migrations/` find the files the diff added under `db/migrations`. A leading `./` matched nothing, and `migrations --since` read 0 files and passed. A `dir` that is the repository itself (`.`, `./`, `db/..`) is refused by name, since it would read 0 files the same way. (release)
- `published` and `changelog` read a `package.json` that is valid JSON but not an object (`null`, a list) as a manifest to excuse or skip, where reading its `private` field threw a `TypeError` and ended the run. (release)
- `migrations --since` names an added file whose path git quotes (non-ASCII characters) as it is spelled, so such a migration is read instead of lost behind a quoted name. (release)
- `smoke` reads a recording or a baseline back through a reader instead of trusting the file: one that does not parse names the file, one that is not a record says which command makes one, and a recording taken before a phase existed reads as having nothing to run for it, where it crashed on `'why' in undefined`. (release)
- A runner reply that is valid JSON but not an object (`null`, a list, a string, a number) is refused as `CannotRun` naming the step, where `null` came back as the reply and failed later on a property read. (release)
- The wrangler TOML reader keeps a `#` and a bracket that sit inside a string (`name = "worker#1"` read as `worke`, and an unbalanced `[` in a route read as an unterminated array), and no table, inline or headed, lets a `__proto__` or `constructor` path reach `Object.prototype` (`[__proto__]`, `a.__proto__.x` and `[a.__proto__]` after an inline table `a`). (release)
- `askRegistry` also answers `published`: the registry's publish time for each version, from the packument's `time` map. (release)
- The smoke's run copy is a repository: the recording's own history when it holds one, else one fresh commit of the snapshot, with the default branch, `origin/<default>` and a local branch for every recorded branch; `origin` is a real bare repository holding the recorded branches and tags, beside the copy, so `git fetch` and `git pull` change nothing. Baseline and run both sit on a candidate branch, and the run commits the applied candidate on it: a gate that reads git (`git diff HEAD`, `main...HEAD`, `origin/main`, `@{upstream}`, a clean-tree check) is measured and sees exactly what the release changed. The copy's git ignores inherited `GIT_DIR`-style and `GIT_CONFIG_*` variables, host ignore and attributes files, hooks and signing; a failed repository step refuses the run (a fresh copy whose snapshot commit failed is NOT MEASURED, with git's stderr), a baseline that could not measure a phase is refused against a run that does, and the headline says so. (release)
- `geonosis-review units <diff>` selects and groups a diff before a reviewer sees it: lockfiles, `dist/`, snapshots and binary files are listed as excluded with the reason, and the rest are cut into units by the two directories they sit under, at most `--max-files` files and `--max-lines` lines each, with `--out-dir` writing each unit's own diff (#513). `apply --diff` refuses a finding whose `line` is outside every hunk of its file in that diff, and a finding may say `"positionUnknown": true` instead of guessing (#514). `review.rules` is a list of `{ path, rule }`: each unit carries the rules its files are on, and `check --diff` asks for each by its whole text in `criteriaChecked` (#517). (review)
- Acceptance criteria are found past a heading shown inside a fence of more than three backticks, a tilde line inside a backtick fence, or an indented fence. (review)
- `apply --to ledger` refuses a `--slug` that names a path (a separator, or nothing) instead of writing `<slug>.md` wherever it resolved, including outside the proofs directory. (review)
- `postgresForSuite({ cluster, connect, migrate, tables })` boots a Postgres cell, migrates it from zero as its owner and returns `reset()` (one truncate over the tenant tables, between files) and `stop()`. The table list is judged before a cluster boots, and a failed migrate or connect stops the cluster it started.  `geonosis-testbed strict` and `runStrict` quote the runner's last stderr lines when the report names no failing test (or there is no report). A runner that died in a native load now says what it printed, instead of "named no failing test".  The README records what sharing one worker costs on vitest 4 (`--no-isolate --max-workers=1`), measured. (testbed)
- `matrix` quotes a command, setup step, connection string, image or runner whose text is YAML syntax (`: `, ` #`, a leading indicator) or that YAML reads as something other than text (`true`, `null`, `~`, `yes`, a number, a date), and escapes a quote in a Postgres major, so the workflow it renders loads. A key outside `testbed.matrix` that is named after an Object property is refused. (testbed)
- `driftBetween` names a variable the themekit emits in another place than the sheet declares it, and still compares a value that sits after a variable that went missing. It compared by position, so a reordering was no drift and a changed value behind a gap was lost. `Drift` gains an optional `moved`. (themekit)
- A batch can report a count per workspace, and a prove says how large its copy was. A per-workspace step with a `batch` takes `counts: "vitest-json"`: its command names `{report}`, the gate runs it with a file there, and the step in the report carries `workspaces`, one `{ workspace, files, passed, failed, skipped }` for every workspace of the batch (a workspace that ran no test is named with zeros, and a file no workspace holds is kept apart as `(outside every workspace)`). A run that left no report it could read fails the step and says so. `also` names paths that always ride with the batch, so a tier runs the whole unit class in one runner process, `plugin/` and `tooling/` included, and the batch is no longer skipped when no workspace is affected. `geonosis-verify` prints the totals under the step and each workspace that failed. `geonosis-verify prove` runs each path a plant reached alone, and calls the step proven only if every one of them goes red with the evidence the plant promised; it replaces `{report}` with a path inside the copy, and each result carries `copyBytes`, printed as `(copy 12.3 MB)`. New exports: `vitestCounts`, `WorkspaceCount`, `bytesIn`. Every step now sees `GEONOSIS_SLOTS`, the heavy slots the run was budgeted, so a command can pass the machine's number to the tool it runs (`--concurrency=${GEONOSIS_SLOTS:-2}`). (verify)
- A per-workspace step can leave workspaces out (`exclude`: paths or globs, refused when one matches no workspace), a step can run `per: "shard"` with `shards` pieces (`{shard}` and `{shards}` in the command), and `prove:steps` looks twice at a plant that went red without its evidence: on a fresh copy with the plant put back, proven when the second red carries it, NOT PROVEN with both tails otherwise.  A tier now waits while a live build holds the tree (`.geonosis/build.lock`) instead of reading `dist/` while it is rewritten, and gives up with the holder named after `GEONOSIS_HEAVY_WAIT_MS`. `@geonosis/ratchet` exports `buildLockPath` and `waitForBuild` for it. (verify)
- `geonosis-verify lint-config` holds the lint a tier runs against the same command at `refs/geonosis/verified`: a rule whose effective level drops or that is gone, a plugin gone, a changed option, a target no longer linted, anything wrapped around `oxlint`, and a diagnostic the mark raised that the tree no longer raises each FAIL, naming what changed. A new rule at error, a raised level and a removed ignore pass. `--accept-config-change` records the person's approval in `.geonosis/lint-config-approval.json`; the geonosis plugin asks for it at Claude Code's permission prompt, listing the changes in plain words. (verify)
- A `geonosis.json` that does not parse is refused with the file named, where `geonosis-verify` printed the bare JSON syntax error. (verify)
- `--affected` and `--require-work` now name a changed file whose path git quotes (a space, an accent) as the file it is, so a change to `my file.ts` no longer reads as a change outside every workspace. (verify)
- `--affected` and per-workspace steps name the workspace manifest that does not parse, where the run refused with a bare JSON syntax error. (verify)
- `--require-work` no longer asks a test of a file that only moved: a changed file whose bytes equal a file the base held at a path that is gone now is not judged. A moved file with any byte changed still is. (verify)
- `geonosis-verify-arch --format html|mermaid|json` draws the `composition-tree` pack's import graph: an interactive page, a mermaid flowchart, or the nodes, edges and per-level counts as JSON. `--output <path>` writes the drawing (required for html); `--check` writes nothing and exits 1 when the file on disk is not byte for byte what would be drawn. Which files start a walk, how paths are printed and grouped, the page title and the colours of a level the ladder does not colour are the pack's `view` option; a tier takes an optional `label`. Over a Workers + D1 app's tree the three outputs are byte for byte what that app's own 1,458-line script writes. `absoluteFilePath` is written only under `view.absolutePaths: true`, so a committed drawing holds no one's home directory. (verify-arch)
- The `medusa` pack reads each file's parsed program (`oxc-parser`, now a dependency) instead of its characters: a regex literal holding a quote, a bracket or a `when(` inside a string, a write named in a template, an annotation held in a string, a `maxRetries` nested under another key and a `subscriberId` spelled in prose are no longer read as code or as a declaration, and a step is named by its `name` key. A file the parser cannot read stops the run with exit 2, naming it, instead of being scanned as bytes (#28–#33). (verify-arch)
- A `geonosis.verify-arch.json` whose top level is `null`, a list, a string or a number is refused by name, as it was for an object with no packs, instead of `Cannot read properties of null`. (verify-arch)
- `geonosis-verify-arch` sets `process.exitCode` and lets the runtime drain stdout instead of calling `process.exit()` after writing the report: a `--json` or `--format ratchet` report past 64 KiB reached a pipe cut mid-token while the exit code stayed normal. (verify-arch)
- A pack named `constructor`, `toString` or `__proto__` is refused as a pack this build does not ship, in the config and in the run. (verify-arch)
- `reconcileFrame` refuses a name that resolves outside the baseline or artifacts directory, and removes a diff left by an earlier run when the failing frame has none of its own (a size mismatch). (visual-diff)
- `link-integrity` refuses an `ignore` pattern that is not a regular expression before it reads a link, naming the pattern, and reports why a target could not be reached rather than `answered 599`. (walk)
- The README's account of what the probes reach is recounted for the thirteen it ships: of the audit's 36 defects, six reproduce from a probe alone, sixteen partially and fourteen by none (it said four, fourteen and eighteen, for the first eight). (walk)
- A `--backend medusa` scaffold carries `packages/backend/src/worker-mode.ts`, the boot assertion that a production fleet is not made only of `MEDUSA_WORKER_MODE=server` processes (no scheduled job, workflow retry or async continuation would ever run), with its own spec. `src/platform.exam.test.ts` reports every case as skipped by name, with `EXAM_DATABASE_URL is not set` as the reason, where the variable is unset. `--kit` with nothing to link is an unmet step, not `ok`. The usage and the plan's usage are built from the arrays that decide `--runtime`, `--backend` and `--ui`, and the README table is held to them. `DomainScaffold.at` and `.field` are `packageDir` and `scopeField`. (create-geonosis)
- `--domains document` composes the document floor's store: the platform opens its seam over `drizzleSessionOver(drizzleOrm)`, `Caller.connection` is a drizzle database, the store is built inside the tenant's session beside the registry, and the app declares `drizzle-orm` at the range the db floor runs on. Where `search` is composed too, its index reads through `executorOver` on the same seam. A platform that composes only `search` keeps the executor port's own seam. The scaffolded exam runs the session and connections exams over drizzle handles in the same case, and passes against a real database. (create-geonosis)
- `create-geonosis` exits 1 when a step it ran after writing the tree failed (the install, the edges-table sync, the format), saying which on stderr; it used to print `FAIL` and exit 0. An offline install that the network retry then completed is not a failure. (create-geonosis)
- `create-geonosis` sets `process.exitCode` and lets the runtime drain stdout instead of calling `process.exit()` after printing the output of a failed step: an install that printed more than 64 KiB lost the tail, which is where the reason is. (create-geonosis)
- `create-geonosis --plan` plans over a repo whose `geonosis.ratchet.json` has a `null` entry or `counters` that are not a list, and whose `apps` or `packages` is a file, instead of crashing; one `readJson` and one `json` replace the copies each file kept. (create-geonosis)
- A scaffolded repo carries `.claude/rules/geonosis/` from day one: every rule file the kit ships, stamped with the version it was scaffolded at, so the doctor's `rules` check passes on a fresh tree. (create-geonosis)
- The `geonosis` package ships `GROUP-CHANGELOG.md`: what one group version bought, every sentence any member carries once with the members that carry it. A member whose version heading held nothing of its own now points there instead of standing blank, which read as "nothing changed" (#356). `pnpm release:cut` writes both. (geonosis)
