# @geonosis/cli

**One declarative command registry, and the CLI door onto it.**

```bash
pnpm add -D @geonosis/cli
npx geonosis --help
```

Every geonosis bin, behind one command, plus the commands that have no bin. The registry is a
list of `{ id, description, kind, inputSchema, run(input) → { ok, output, exitCode } }` and nothing
else reads a second list: `@geonosis/mcp` serves *these* commands, and the plugin calls *these*
commands. Four front doors over one registry is musa's shape; Midday proves it at 103 tools, where
the in-app assistant is an `InMemoryTransport` client of the same server rather than a second
implementation.

## What a pnpm consumer installs directly

pnpm links into `node_modules/.bin` the bins of a direct dependency only; npm also links the bins
of the packages it hoists. A direct dependency on `geonosis` (the metapackage) or on
`@geonosis/cli` gives the one command every manager links: `geonosis`. Every tool is reached
through it — `geonosis ledger plan check`, `geonosis ratchet`, `geonosis review check …` — because
the door runs the repo's own copy of the tool when it has one, and the copy that came with
`@geonosis/cli` when it does not. The plugin's skills, rules and commands spell the ledger that way.

A tool's own bin (`geonosis-ledger`, `geonosis-ratchet`, `geonosis-review` and the rest) is a
command under pnpm only where its package is a direct dependency, on the kit's version. Nothing in
the kit needs it: the geonosis plugin's hooks run the repo's own `geonosis-<tool>` from
`node_modules/.bin` where it is linked, else the repo's `geonosis` door, and only then a copy on the
`PATH`; the commit and push guard refuses a commit it cannot check. A pnpm repo that installs only
`geonosis` is checked through `geonosis ledger …`. `geonosis update` keeps every `@geonosis/*` pin
on one version.

## The commands that front a bin

`ratchet` · `doctor` · `verify` · `lint-parity` · `verify-arch` · `visual-diff` · `ledger` ·
`walk` · `release` · `review` · `testbed` · `observability` · `status`

Each one **spawns** its bin. Nothing is re-implemented, so nothing can disagree with the tool it
fronts, and **the child's exit code is passed through unchanged**: 0 clean · 1 a finding or a
refusal · 2 a run that could not be made at all (D-030). A wrapper that normalised a 2 into a 1
would have invented a finding out of a tool that never ran.

The door runs the repo's own copy of the tool: the package's bin in a `node_modules` 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. A resolver also took the pnpm virtual store a pnpm bin
shim puts on `NODE_PATH`, bun's global install cache and a copy above the repo, so another
version's copy ran unannounced. With no copy in the repo the door runs the one that came with
`@geonosis/cli` and says so on the first line of its stderr, because a fallback nobody can see is
the version skew `geonosis doctor` exists to catch. The tool's stdout is the door's stdout and its
stderr the door's stderr, so `geonosis doctor --json` prints the doctor's report and nothing else
on stdout. Under `geonosis --json` the two are joined in `output`.

The door starts a tool with `GEONOSIS_DOOR` naming the bin it started (`geonosis-ratchet=geonosis
ratchet`), and the tool prints its usage and the commands it advises in that spelling,
`geonosis ratchet [--cwd <dir>] …`, the one that runs under every manager. A tool that tool starts
keeps its own name, and a tool run by its own bin prints its own name.

For a tool it fronts, the door loads only what it runs, not the whole command registry. Measured on
bun 1.4.3 at load 19–20, `geonosis ledger delivery check --cached` took a median 204 ms against
125 ms for the ledger's own bin (379 ms before), which is what lets the plugin's commit and push
guards run the ledger through the door inside a 2.5 s budget.

## The runtime the bins start on

Every bin starts on bun (`#!/usr/bin/env bun`), whatever the manager that installed it. A machine
without bun on its `PATH`, CI as a rule, fails with `env: bun: No such file or directory`. The
package manifests name bun in `devEngines.runtime`, which no manager warns on in a dependency; the
way to have bun there is the manager's own, each measured on a scratch tree with bun off the `PATH`:

| Manager | What to do | What it does |
|---|---|---|
| pnpm 11.8.0, 12.10.1 | `"devEngines": { "runtime": [{ "name": "bun", "version": "^1.4.2", "onFail": "download" }, { "name": "node", "onFail": "ignore" }] }` in the root `package.json` | installs bun as a dev dependency; it is on the `PATH` of `pnpm exec` and of scripts |
| yarn 1.22.22 | `yarn add -D bun@^1.4.2` | `devEngines` installs nothing here; the devDependency links `node_modules/.bin/bun` onto the `PATH` of `yarn <bin>` and of scripts |
| npm 11.13.0 | `npm install -D bun@^1.4.2` | the same, for `npm run`, `npm exec` and `npx` |
| bun | nothing | it is the runtime |

A bin called as `./node_modules/.bin/<bin>` from a bare shell finds bun only if the shell's `PATH`
does. `geonosis doctor` names the row for the manager the tree uses (`packageManager`, else its
lockfile) and says nothing where no installed kit package asks for bun.

## Under a release-age cooldown

A release is not installable for as many days as the manager's cooldown says, so a consumer with one
is a version or two behind the kit's own releases. The exemption that exists is per manager,
each measured on 9 October 2026 against a registry serving a package published two hours earlier:

| Manager | The setting | Exempting the kit's own packages |
|---|---|---|
| pnpm 11.8.0 | `minimumReleaseAge`, minutes | `minimumReleaseAgeExclude: ['geonosis', 'create-geonosis', '@geonosis/*']` in `pnpm-workspace.yaml`. The glob exempts every `@geonosis/` name, and a young package outside it is still refused with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` |
| bun 1.4.2 | `[install] minimumReleaseAge`, seconds | `minimumReleaseAgeExcludes = ["@geonosis/cli", …]` in `bunfig.toml`, each name exact: `"@geonosis/*"` excludes nothing and the install fails with `blocked by minimum-release-age` |
| npm 11.13.0 | `min-release-age`, days | none. It becomes one `before` date for the whole tree, and `--min-release-age=0` lifts it for every package, third-party ones included. Wait for the release to age, or lift it for one install and read the lockfile for what came younger than your cooldown |
| yarn 1 | none | nothing to exempt: yarn 1 has no cooldown |

`geonosis update` reads the cooldown of pnpm, bun and npm, stops before writing when a kit package is
younger than it, and prints the exemption for your manager. It never lifts the cooldown itself. Under an
exemption, your manager's cooldown still governs every third-party package. When you pass the manager's
override in `--install-args`, `update` takes it at its word and holds nothing back. After the install it
lists each third-party version the lockfile gained that is younger than your cooldown, as `young` lines,
whichever kit package brought it in. A version the tree already had and kept is not listed.

The kit's own cut holds what it publishes to the same kind of age: `pnpm release:check` refuses a range whose every acceptable release is younger than `release.thirdPartyAge` days (7 by default). So pnpm and bun, which can exempt the kit's own names, can always install the kit under a cooldown. npm has no exemption, and its override lifts the age for every package.

## An npm tree on a kit older than 3.5.0

The installed copy's `update` runs a bump, and before 3.5.0 it refuses an npm tree ("nothing here says which package manager to use"). Its `--dry-run` reports what it would repin all the same. Run the target version's own `update` instead, fetched for this one run:

```bash
npx -y --min-release-age=0 --package=geonosis@<version> -- geonosis update --to <version> --install-args=--min-release-age=0
```

`--min-release-age=0` on `npx` lifts npm's cooldown for that fetch alone. The one in `--install-args` lifts it for the bump's two installs, and the `young` lines then name what came younger than the cooldown. That copy runs the whole ritual, the lock proof and the doctor included, and then its own `post-update`.

## The commands that are the CLI's own

| Command | What it does |
|---|---|
| `geonosis init --presets a,b [--brand x] [--runtime pnpm\|bun]` | Writes `geonosis.json`, `.oxlintrc.json`, `geonosis.ratchet.json` and `gate-baseline.json` into an EXISTING repo — each one that is absent; each one that exists is kept byte for byte and named (`kept … already yours`), never overwritten and never merged into — and copies the rule files as `geonosis rules` does. It measures the census rules of its presets first (below). Its fast tier records each counted tool through the door, `pnpm exec geonosis ratchet --record <counter>` (`bunx geonosis ratchet …` on bun), and its full tier ends with `pnpm exec geonosis ratchet`, or `pnpm ratchet` where the root declares that script: under pnpm `geonosis-ratchet` is a command only where `@geonosis/ratchet` is a direct dependency, so the root is not asked to declare `@geonosis/ratchet` or `@geonosis/verify`. `init --help` prints the syntax; a flag it does not have is refused by name. |
| `geonosis rules` | Copies the rule files this kit version ships into `.claude/rules/geonosis/` — copies, because Claude Code loads a rule symlinked from outside the repo only after approval, and then only one with no `paths` list. A `# geonosis-kit <version>` line sits directly under each file's opening `---` (a marker above it would stop Claude Code reading `paths`). A marked file the kit no longer ships is removed; a file you added beside them is left alone; a directory that is a symlink is refused, never written through. `init` runs it; `update` runs `geonosis post-update`, which does; `geonosis doctor`'s `rules` check warns when one differs, and its advice names this command first. A repository settles what it receives in `geonosis.json → rules`: `rules.omit` lists files it does not take (`["gates.md"]`; a copy the kit wrote is removed), and `rules.overrides` maps a file to the repository's own text (`{ "gates.md": "docs/gates.md" }`), written with the marker; `update`, `rules` and the doctor all read both. The gate command a file names is written in the repository's own package manager (`bun run verify` in a bun repository). |
| `geonosis post-update [--from <version>]` | What the kit version installed here does in a repo after an update, as the steps that version ships, in order: `rules` writes the rule files as `geonosis rules` does; `check` checks them with the doctor's `rules` check; `agents` regenerates an `AGENTS.md` whose header says the ledger wrote it, where this kit's ledger would write another (`geonosis ledger sync --target agents-md`), and leaves any other `AGENTS.md` as it is; `census` adds the census rules of the presets in use (below) to a repo that lacks them; `replaced` names a dependency on a package the kit renamed (the pairs are in the `release-renames.json` this package ships); `runtime` names the way to have bun under this tree's manager; `tiers` moves a record step an older init wrote with the ratchet's own bin to `geonosis ratchet --record`, which runs where only `geonosis` is installed, names each step it moved, leaves every other byte of `geonosis.json` as it was and moves nothing on a second run; `once` names a tool the gate runs once as a step and again inside a ratchet counter, with the step to replace and the `"output"` to add; `squawk` names the optional peer a migration dialect needs and does not have; `doctor` runs the doctor checks the updater that ran did not ask for; `notes` prints the group release notes from `--from` up to this version — the number of entries, each one starting `BY HAND:`, and the path of `GROUP-CHANGELOG.md` in this package. A BY HAND entry 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 steps of this run do — and one ruled out, or done by a step that passed, is counted and not listed. It records the kit version it ran for in `.geonosis/post-update.json`. From 3.3.0 on, `geonosis update` runs the copy it installed, not its own, so a step a later release adds reaches the repo; after an update run by 3.2.0 or older, run `geonosis post-update --from <old version>` — `geonosis doctor` warns until it has run. Exit 1 when a step fails. |
| `geonosis sync [--check] [--target turbo\|layer-walls\|restricted-imports --into <file>]` | Generates `.cursor/rules/*.mdc` from the law and this repo's skills — one law, every tool (D-002). Deterministic, so `--check` is a gate. `AGENTS.md` is NOT written here: it has one generator, `geonosis sync --target agents-md --into AGENTS.md`. With `--target` it hands the question to `geonosis ledger sync`, exit code and all. |
| `geonosis discover [--config <file>] [paths…]` | Reads the findings this repo's own oxlint config already produces into `fixShape` candidates — rule, count, first three sites. It writes nothing: the scored agent never writes the scoreboard. |
| `geonosis update [--to <version>] [--floors latest] [--floor <pkg>@<v>] [--dry-run] [--fresh] [--verbose] [--prove]` | Bumps every kit pin in every workspace to one version, in the order that makes it safe: every target resolved against the registry before anything is written, the manifests repinned, the nested copies purged, an install, a frozen install, the doctor before and after, `geonosis post-update` run by the copy just installed, the repo's `fast` tier, and the oxlint findings of a plugin bump counted before and after over every oxlint config that loads the plugin. `geonosis update --help` prints each step, where it finds the workspace root, the managers it reads and refuses, the floors, the renames and the release-age handling. `install.allowScripts` in `geonosis.json` names packages whose lifecycle scripts run after the frozen install's `--ignore-scripts`. It writes each step's start and end, with its time and outcome, to stderr as it goes, and a line a minute while a step runs. A WARN or FAIL the doctor finds after the repin and not before, told by check and subject, is a BY HAND item, kept in `bump-report.json` under `byHand`. A re-run resumes after the install, lock, doctor and post-update steps already green over the same tree (`.geonosis/green-update-<step>.json`; `--fresh` runs them again), and a timed-out verify names the steps that had passed. `--verbose` lists every package of a release family the `young` lines group into one. `--registry <dir>` installs a packed candidate; a second run over it lists the release notes above the version the tree came from, read off the last commit, and under a lifted release-age gate it names the young third-party versions the candidate brought in, asking `--npm-registry` when each was published. |
| `geonosis portfolio [--registry <url>]` | One row per consumer named in `geonosis.json` → `portfolio.consumers` (`{ name, artifact }`), read off the artifact that consumer's CI uploaded (its `.geonosis/`: `envelopes/*.json`, `gate-report.json`, `bump-report.json`): the kit version it runs, its lag in days behind the latest `@geonosis/cli` by the registry's publish dates, its last green tier, the lines its last bump deleted. Exit 2, and no row, when an artifact is missing or will not parse. |
| `geonosis explain <rule\|counter\|tier\|package> [--json]` | Prints the effective value of everything a name decides, beside the file that decided it. |
| `geonosis skills list\|lock\|audit` | `skills-lock.json` by content hash (`source`, `sourceType`, `skillPath`, `computedHash`). `audit` fails on drift, on a skill nobody pinned, and on an **executable file inside a skill directory** — the Vercel incident class a markdown hash cannot see. |

### Census-first rules

`init`, and `post-update`'s `census` step, which `update` runs, measure a rule before they enforce
it: `typescript/no-explicit-any` for `frontend`, and with `i18n` the react plugin's rules that catch
real bugs (the plugin's `censusOf` and `CENSUS_PRESETS` are the list both read). 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. `post-update` asks oxlint what `.oxlintrc.json`
resolves each rule to (`oxlint --print-config`) and leaves a rule that resolves to on, under any
spelling, at any level, through `extends`, an override or a category. A config oxlint cannot
resolve fails the step, and nothing is written. Removing 475 explicit `any`s from one storefront
found five live bugs they had hidden, among them a price sort subtracting a field variants do not
have.

### What `init` will not write

An enabled rule without the options it needs judges nothing and says so every run (D-084), so a
skeleton that wrote one would start an adopting repo red over a question only that repo can answer.
`init` therefore enables only the rules it has answers for, and **names the ones it left out and the
option each wanted**. Which rules those are is not a list in this package — it is measured by
constructing every rule with no options and keeping the ones that ask for one, so a rule added to
the plugin cannot be forgotten here.

### What the ROOT declares

`@geonosis/cli` (or the `geonosis` metapackage over it), plus the bins the root's own scripts call
**by name**, and nothing the door already owns. A package a workspace imports is that workspace's
to declare.

A second declaration of a package the door brings is a second version that can disagree with it,
and every tool that asks what a repo uses reads it as unused — one consumer's root carried
`@geonosis/doctor` and `@geonosis/visual-diff` that way, and knip flagged both. `geonosis init`
prints this rule with the bins it just wired, and `geonosis doctor --only group` names any root
declaration that breaks it.

## `--json`

`geonosis --json <command>` prints `{ command, ok, exitCode, output }` — the registry's structured
answer, which is the same object the MCP door returns. `geonosis --version` (or `-v`) prints the
door's own version.

`parseJsonc`, exported from the package root, reads JSONC the way oxlint and wrangler do
(`jsonc-parser`): a comment between two tokens is refused, a line ending in a carriage return alone
is read, and the `SyntaxError` it throws names the error and its line.

A flag written **before** the command is this door's; everything after the command belongs to the
command, `--help` included. Scanning the whole line for `--help` made `geonosis testbed --help`
print the door's own usage and never run the bin — the wrong answer, delivered confidently.

## Layer

`cli` may import `plugin-api` (the presets and the rules' own refusals), `parity` (oxlint
resolution and finding parsing) and `doctor` (package resolution). It may import **no gate it
fronts**; every one of those walls is planted against in the plugin's `dogfood.bin.test.ts`.
