npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@geonosis/ledger

v3.1.0

Published

The tick ledger — validate and refuse, never author: no criteria, no proof, no runtime code, no revisit trigger, no delivery.

Readme

@geonosis/ledger

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

The tick ledger. It validates, refuses, and appends one row. It authors nothing else.

That restraint is the point. The kit survey found Spec Kit writing 2,100 lines of specification for 600 lines of TypeScript, and this repo's own plans 002–016 were written and never used; the plans that shipped were written the day the work happened. So plan new writes a skeleton whose one criterion is a placeholder that plan check refuses until a person replaces it, and no command here generates a document.

npm i -D @geonosis/ledger
npx geonosis-ledger plan check

Exit codes are the same everywhere: 0 passed · 1 a refusal, with its reason on stdout · 2 the run could not be made at all (bad config, unknown flag), with the reason on stderr.

plan check [<file>|<dir>] [--since <NNN>]

A plan needs three things, and every violation is printed as <file>: <what is missing>:

| Refusal | What it means | |---|---| | no status header | nothing in the file matches plan.statusHeader | | no "Acceptance criteria" heading | no heading matches plan.criteriaHeading | | no EARS criterion under "Acceptance criteria" | the heading is there and nothing under it — under it, bounded by the next heading of the same depth — matches plan.criteria | | no "Verification" heading | no heading matches plan.verificationHeading |

--since <NNN> exempts every lower-numbered plan: they print as <file>: legacy and hold nothing up. A gate that refuses the whole back catalogue on the day it is installed is a gate that gets uninstalled.

Declare it once as ledger.plan.since in geonosis.json and every caller gets it — the flag then overrides the declaration for one run. A threshold every caller has to remember is one nobody applies: one consumer's status read 28 pre-contract plans as UNDECLARED noise on every call, because only plan check --since knew the number. status reads the declaration too, and its plans line separates the legacy catalogue from what is live.

Only NNN-slug.md files are read. A README.md in the plans directory is not a plan.

plan new <slug> [--discovered-from <NNN>]

Writes <plans>/<next NNN>-<slug>.md — twelve lines: the title, the status header, the two headings, and two HTML comments. The number is one past the highest on disk, never the count, because counting reuses a number after a delete. It refuses a slug that is not kebab-case, and a slug some existing plan already carries (naming that file).

--discovered-from <NNN> writes the provenance line — > **Discovered from**: plans/021-one.md — what that plan's work turned up — and refuses (exit 2) a number no plan file carries. A citation to a plan that does not exist is worse than none: a reader chases it once, and stops believing the next one.

The plan graph — plan ready and plan claim <NNN>

A plan says what it comes after on its status header line, in brackets:

> **Status**: DRAFT · **Milestone**: M2 · after: [021, 023]

One line is authoritative. A plan whose status says ACTIVE on line 3 and whose dependencies live in a paragraph on line 40 has two answers to "can this start", and the second one is never read.

plan ready prints every plan whose edges are all done, then every blocked plan with its blockers, then a counted verdict line. It is a report, not a gate: exit 0 either way.

ready    plans/023-three.md (DRAFT)
BLOCKED  plans/024-four.md — after 022 is ACTIVE, not DONE
plan ready — 3 ready, 1 blocked, of 4 plan(s)

plan claim <NNN> [--by <name>] records the claim in .geonosis/plan-claims.json — runner-owned, like the gate report. It refuses (exit 1, reason on stdout):

| Refusal | Why it exists | |---|---| | <file> comes after 022 is ACTIVE, not DONE | claiming work whose blocker is still open is how two plans land in the wrong order | | <file> is already claimed by w39 at <ISO> — one plan, one holder | two sessions cannot see each other's worktree; the claims file is where they can | | 022 is not a plan in plans | a claim on a number nobody wrote |

Three refusals the graph does not make, said out loud: an edge to a plan that is not on disk blocks (treating "cannot find it" as "finished" is the silent fallback this kit refuses); an edge that is not a number — after: [W28] — is a hard error naming the file, never a dropped edge; and a cycle is not detected as such, it simply never becomes ready, with each plan naming the other as its blocker.

plan.done decides which status word counts as finished (default ^DONE$), because that word is a repo's own: this one writes DONE 2026-08-30, and a kit that decided for everybody would read SHIPPED — NOT ADOPTED as a plan nothing waits on.

plan drift — is this plan still about this tree?

A plan is written at a moment and about some paths. Once it says so, "is this still true" is answerable instead of remembered:

> **Status**: ACTIVE · plannedAt: 87d9db5 · scope: [packages/ledger/src/, docs/rules-backlog.md]

plan drift compares the two, and its verdicts are deliberately unequal:

| What git did to a file in scope | Verdict | |---|---| | renamed | STOP — every line of the plan citing that path is now a citation to nothing | | deleted | STOP, same reason | | edited | changed — that is the work the plan asked for, not drift | | a scope matching no file at all | STOP — a glob that fits nothing reads exactly like a scope nothing touched | | plannedAt naming no commit | STOP — an anchor nobody can follow |

A plan declaring neither anchor is skipped by name, with the reason, because a skip nobody can chase is the PASS — 0 plan(s) shape one level down. Exit 1 on any stop, exit 0 otherwise, and the last line always carries the counts.

tick --plan <NNN> --commit <hash> --proof <path> [--note "…"]

The gate. Its refusals, checked in this order, each exit 1 with its reason on stdout:

| Refusal | Why it exists | |---|---| | <ref> is not a commit | a ledger row that names a hash nobody can resolve is a claim | | plan <NNN> does not pass plan check: … (every violation listed) | a tick against a plan with no criteria has nothing to have satisfied | | no plan <NNN> in <plans> | the row would point at nothing | | <path> does not exist · <path> is a directory | musa's rule and a Medusa storefront's G3: the proof is the evidence, not the summary | | <hash> ships no runtime code (G0). Its diff: … | a Medusa storefront's G0, returned (D-025): "a tick MUST deliver real runtime code". Docs, tests and ledger updates may ride along; they may never stand alone. The whole diff is printed so the refusal is checkable | | the commit message is <n> line(s) · every changed line is a comment or whitespace | the two delivery gates below, asked of the commit being ticked: the narration is not the work, and a comment is not a delivery |

On success it appends one row to ledger.progress — | tick | date | plan | commit | proof | note | — creating the file with its header when absent. That append is the only write it makes, and a refused tick makes none at all. The tick number is one past the highest already in the table, never the row count, because counting reuses a number after a delete.

G0 reads git diff --name-only <hash>~1 <hash>, which is a Medusa storefront's own spelling. A root commit has no ~1, so that one case diffs the tree against the empty tree instead — otherwise a repo's first commit could never be ticked. The ledger runs git only in the repo it was pointed at.

land <branch> [--tier <tier>] — the gate runs on the MERGE RESULT, and the gate merges

The incident this closes: a red verify was merged past twice in one day, through two different spellings — verify | tail && git merge (the pipe reported the pipe's exit) and reading a wrapper's exit instead of the gate's own tail. Neither spelling was dishonest; both were available. land exists so that class cannot be spelled at all: the thing that runs the gate is the thing that does the merge, and there is no exit code in between for anybody to read wrong.

geonosis-ledger land feature-branch --tier fast
  1. Refuses a branch that is not a fast-forward from HEAD, before any gate runs.
  2. Checks the branch out detached in a temp worktree — the merge result.
  3. Runs ledger.land.prepare there (if any), then <ledger.land.verify> <tier>.
  4. Green → git merge --ff-only, and an attestation is appended to .geonosis/land-attestations.json: { attestedAt, branch, report, tier, tree }. The gate report is copied out of the worktree to .geonosis/land/<tree>.json before the worktree is destroyed, so the evidence outlives it.
  5. Red → nothing is merged, exit 1, and the refusal quotes the gate's own tail: the failing step's id, its exit code, and the lines it printed.

Why ff-only, measured rather than assumed: with merge --ff-only the merge result is the branch's tree — git rev-parse <branch>^{tree} and the root's HEAD^{tree} after the merge are the same hash. So gating the detached checkout gates exactly the bytes that land, with no merge commit to build and nothing to reconcile. A branch that cannot fast-forward could not have landed ff-only however green it was, so it is refused with "rebase it and ask again" rather than gated for nothing.

The tree hash, not the commit hash, is what an attestation is about: a rebase, an amend and a cherry-pick all make a new commit for a tree that has already been read.

The temp worktree is removed in a finally, green or red — a worktree left behind holds the branch and the next land fails for a reason that has nothing to do with the branch it was asked about.

land deliberately does not check that the working tree is clean: git merge --ff-only refuses on its own when local changes are in the way, and its refusal is quoted rather than pre-empted.

The land block

"land": {
  "tier": "fast",                     // which tier land runs (default "full")
  "verify": "geonosis-verify",        // how this repo spells its gate runner
  "prepare": "pnpm install --frozen-lockfile",  // shell run in the merge-result worktree first
  "branch": []                        // branches whose push needs an attestation — empty = inert
}

The consumer names the cost. A Workers + D1 app's full verify is 25 minutes and their ruling is that nothing blocks a push for more than ten seconds, so the tier land runs is theirs to choose and the long one stays in CI (challenge C1). prepare exists because a fresh worktree has no node_modules, and a tier that fails for the want of them is a red nobody caused.

land <branch> --attest-only / --from-attestation — the gate that does not make you wait

A repo whose gate is twenty-five minutes cannot run it during a push. So the evidence is recorded before and read after:

geonosis-ledger land feature --attest-only     # runs the tier, records the green, merges nothing
geonosis-ledger land feature --from-attestation # merges iff that record still fits this tree

--from-attestation runs no gate at all. It merges only while a recorded attestation matches the branch's current tree hash, at the tier land would have run (--tier, else ledger.land.tier). One more commit and the hash moves, and the record no longer speaks for it. A record taken at a cheaper tier does not stand in for the one asked for. The refusal names the tree it could not find:

land REFUSED — no attestation for feature's current tree 4f2a… at the full tier (attested at fast only) — run `land --attest-only feature --tier full` first

land --guard-push [--branch <name>] — opt-in, and inert until it is not

"land": { "branch": ["main"] }

With branch empty — the default — the guard prints push guard inert and exits 0. That is deliberate: a Workers + D1 app commits straight to main a hundred times a day, and a default-on refusal there is a gate that gets uninstalled the same afternoon (C1). For a branch a repo does name, the push is refused unless the tree at HEAD is one some earlier run attested.

The Bash hook wires this: a git push is handed to land --guard-push with the branch the push would move — the destination side of a refspec, since HEAD:main moves main. The hook keeps no list of protected branches of its own; the list is the consumer's, and a second copy is a second answer waiting to disagree.

proof capture <slug> --command "<cmd>" [--url <url>]

Runs the command in the repo root and writes <proofs>/<next NNN>-<slug>.md: the HEAD hash and subject, the command, the URL if given, the exit code, and the combined stdout/stderr bounded to ledger.maxLines (default 200) with … N more lines stating what was dropped.

The process exits with the command's own exit code. A failing proof is recorded and still fails; a capture that swallowed the red would be a gate reporting green over a run it had just watched break.

oath --plan <NNN> --text "…" --files <path,path> and oath check

The covenant, mechanised. One source repo kept 61 KB of prose oaths — obedience to articles, to a hierarchy, to a law — and not a line of it could be checked afterwards, including the line listing which files had been read. What survives the translation is the part a machine can hold:

geonosis-ledger oath --plan 024 --text "feat(ledger): the oath verb" --files skills/geonosis-code/SKILL.md
oath sworn at 2026-09-01T10:00:00.000Z on plan 024
  planned commit  feat(ledger): the oath verb
  read            CLAUDE.md (137 lines, 4f2a1c9b3e07)
  read            skills/geonosis-code/SKILL.md (165 lines, 8c31d0a5f2be)

ledger.law is read whether it was listed or not — it is the thing being bound to, and an oath recording everything except the law is the prose again. A file that is not there is a refusal: a receipt for a file nobody read is worse than no receipt.

oath check compares the last oath with the bytes on disk now, exit 1 naming anything that moved:

CHANGED  CLAUDE.md changed since the oath of 2026-09-01T10:00:00.000Z (137 lines read, 141 now)
oath BROKEN — 1 of 2 file(s) read are not the bytes that were read

That check is what makes it a gate rather than a diary. A law edited under the reader is a law they did not read.

journal --kind stub|mock|guess|skip --where <path:line> --why "…"

Appends a dated row to ledger.journal. The four kinds are the four ways a delivery is not the thing it claims to be, and no kit in the 2026 survey asked an agent to record where it stubbed — which is why a stub reads as done in every report after it. An unknown kind is refused (exit 2), as is a --where without a line number and an empty --why. Pipes inside a why are escaped, so prose cannot break the table.

decide --id D-NNN --text "…" --why "…" --evidence "…" --revisit "…" [--status "…"]

Appends one row to ledger.decisions. Values go under the column that names them, not under a fixed position — one register has no Status column and this repo's had no Revisit trigger, and both are legitimate. A column the flags do not fill gets —.

| Refusal | Why | |---|---| | a decision needs a revisit trigger … | C10. A register whose entries carry no event that would reopen them is a diary. PROPOSED with no stated trigger is a decision nobody ever has to make | | <file> has no "Revisit trigger" column (C10). Its header has to become: <the exact line> | the tool prints the header the register needs and never writes it. A tool that rewrites the shape it validates has become the source of truth for that shape | | <file> already carries D-NNN | ids are unique or the register cannot be cited | | "<id>" is not an id of the form D-NNN · <file> does not exist · <file> has no table in it | |

A blank line inside a long register splits the table for a markdown renderer, but the rows after it are still the register. decide steps over the gap, so a new row lands after the last row rather than in the hole. (This repo's own register had exactly that gap, at D-039/D-040.)

handoff [--dry-run] [--check]

Rewrites the ## SESSION CLOSE block of ledger.handoff, between

<!-- geonosis-ledger:session-close -->  …  <!-- /geonosis-ledger:session-close -->

creating the marked block at the end of the file when the markers are absent, and leaving everything outside them exactly as it was. The block holds HEAD, uncommitted files, the last tick row, the last gate report (.geonosis/gate-report.json: tier, ok, finishedAt), and every plan whose status is not DONE/SHIPPED/CLOSED/ABANDONED and not the repo's own ledger.plan.done word.

It is a pure function of the tree — no clock, no run id. Two runs over one tree are byte-identical, which is a test. The handoff file is excluded from its own uncommitted list: writing the block dirties the file, and without that exclusion the second run reports the first run's write.

--dry-run prints the block and writes nothing. --check writes nothing either, and exits 1 when the block on disk is not the one the tree gives now, saying whether HEAD moved on from under it.

status [--json]

The digest: six lines, always the same six, whatever the size of the repo behind them — a hundred plans and a hundred ticks print six lines. A digest is a perception surface, and an unbounded one is the context window it exists to protect. A contract test pins the exact shape.

HEAD      feat(ledger): tick, proof capture and journal
plans     ACTIVE 2 · DONE 1
last tick | 1 | 2026-08-30 | 002 | abc12345 | proofs/001-a.md | — |
gate      full — FAILED at 2026-08-30T10:55:00.000Z
baseline  6 counter(s)
journal   0 fallback(s)

Nothing here re-runs or re-judges a gate; it reports what the gate report and the baseline already say. A line whose fact is missing says so (no tick yet, no gate report) rather than disappearing, so the shape never moves. The baseline is where geonosis.ratchet.json's baseline says it lives, else gate-baseline.json, the same answer as the ratchet's resolveBaseline, and is read as JSON — the ledger never imports the ratchet, so it is never one call from rewriting the number it reports.

burndown --from <rev> [--to <rev>] [--json]

Opened, closed and carried between two states of ledger.backlog: the file at <rev> against the file at --to, or the working tree when --to is not given. A row is a numbered table row, and its last cell is where it stands, matched against ledger.backlogDone. A row filed and finished inside the window counts as opened and as closed. A row open at both ends is carried. A row that was there before and is gone after is listed as vanished, because nobody closed it.

It is a report: it exits 0 whatever it finds, and 2 when git cannot show the file at <rev>. #292 was fifty-five rows filed in one day, and a cap on filing would have hidden them; the release review carries the arithmetic instead.

The delivery gates — commit-msg and delivery check

Two ways a delivery can be nothing while looking like something: the narration outgrows the work, and the diff is all comments. Both refuse with exit 1, and tick runs both on the commit it is given.

commit-msg <file> | --message "<text>" | --commit <hash>

Refuses a message longer than ledger.commitMessageLines — default 4: a subject and three body lines. A message that needs a fifth is a delivery that should have been two commits, or a paragraph that belongs in a proof file.

What is not counted, and why:

| Not counted | Why | |---|---| | blank lines | they are the format, not the message | | lines beginning # | a commit-msg hook is handed the raw editor file, git's whole # Please enter… template included | | everything past # --- >8 --- | commit -v puts the entire diff below the scissors | | a trailing block of Token: value trailers | Co-authored-by:, Signed-off-by:, Refs: are metadata git appends; counting them would ration the body by how much tooling a repo runs |

The trailing block is git's own definition, so a Note: … in the middle of a body is counted. The subject is never stripped, even though feat: a thing is character-for-character a trailer — that bug read every one-line message as empty, and it has a test.

Usable directly as a hook:

# .husky/commit-msg  or  lefthook commit-msg
npx geonosis-ledger commit-msg "$1"

delivery check --cached | --commit <hash>

Refuses a diff whose every changed line is a comment or whitespace. Comment tokens are per language, chosen by extension — # opens a comment in a shell script and a heading in markdown, so the token alone cannot decide:

| Extensions | Comment tokens | |---|---| | ts tsx js jsx mjs cjs mts cts go rs java kt swift c cc cpp h scss less | // /* */ * {/* */} | | css | /* */ * | | sh bash zsh py rb toml ini cfg conf env properties mk gitignore, Dockerfile, Makefile | # | | yml yaml | # | | md mdx html xml svg vue svelte astro | <!-- --> | | sql | -- /* */ * |

A blank changed line counts as commentary too, so a whitespace-only change is refused. A file whose language is not in the table has its changed lines treated as code — the safe direction for a gate that refuses.

Deleting comments counts as a comments-only delivery. A comment sweep is real work, and it rides along with runtime code exactly the way G0 lets docs ride along; it does not stand alone.

--prove

  PROVEN commit-msg: refuses a 40-line commit message
  PROVEN delivery check: refuses a diff whose every changed line is a comment

prove PASS — every delivery gate refused the thing it forbids.

It plants both forbidden shapes for real — the comments-only diff into a throwaway git repo it initialises and deletes — and requires both to be refused. A probe that comes back clean prints CANNOT FAIL and exits 2, the same status as a measurement that could not be made: a gate that has never been shown refusing is a claim, not a gate.

architecture check [--no-workspace] and sync — one table, three spellings (C8)

An allowed-edges table is typically written three times in a monorepo's tooling: turbo's boundaries.tags, per-package no-restricted-imports, and layer-walls. Three spellings of one fact is an SSOT violation inside the enforcement itself. So the table is the source and the three configs are generated from it.

ledger.architecture must contain a table whose header is | tag | packages | may depend on | (whitespace- and emphasis-insensitive):

| tag           | packages        | may depend on                   |
| ------------- | --------------- | ------------------------------- |
| `domain`      | domains, utils  | —                               |
| `storage`     | db              | domain                          |
| `sdk`         | client          | host-worker (TYPE only), domain |

architecture check exits 1 listing: a packages cell that names no workspace directory (matched against the directory, its basename, or the package name — repos write all three), a may depend on entry that is not a declared tag, and a tag declared twice. --no-workspace skips the first of those.

A parenthetical like (TYPE only) is a note, not a violation: the tag is taken and the annotation reported, because none of the three generated spellings can express "type-only" and a fact the tooling silently discards is a fact the tooling has decided is untrue.

sync --target turbo|layer-walls|restricted-imports [--write <file>] [--check <file>] [--package <name>]

| Target | Generated | Written into | |---|---|---| | turbo | { tags: { <tag>: { dependencies: { allow: [...] } } } } | the boundaries key | | layer-walls | [{ name: <tag>, paths: ['^<pkgname>', '<dir>/'], mayImport: [...] }] | the layers of the biological-architecture/layer-walls rule | | restricted-imports | per package, every package its tag may not depend on | the paths of that package's no-restricted-imports rule |

restricted-imports is per package, so --write packages/db/.oxlintrc.json infers the package from the file's directory; --package overrides it.

Nothing is inferred. If a tag may depend on storage and storage may depend on domain, that does not make domain reachable — the table is spelled in full or it does not say what it says. A computed transitive closure would silently widen an edge no author wrote.

--write splices only the named fragment. JSON.parse → JSON.stringify would reformat the whole file, which turns a one-key sync into an unreviewable diff and a fight with the repo's formatter. The value's byte span is located by scanning and replaced in place, so $schema, tasks, and every other rule come out identical. --check exits 1 and prints what is committed beside what is generated.

A key that is not there yet is created — a turbo.json with no boundaries, a config that has never named layer-walls, a package whose no-restricted-imports has not been written. Still only that fragment: everything already in the file stays where it was, and a rule is created as ["error", { … }], the severity being the one thing a generator cannot read off the table. A repo adopting this starts from its own config, not from a shape hand-copied out of these docs.

The adoption evidence is in proofs/, under plan 022's W4 sync: run read-only over a copy of a Workers + D1 app, architecture check found a may depend on cell naming a package instead of a tag, and sync --target turbo --check showed a config with five tags against a document with seven — sharing one name, sdk, which means different things on the two sides.

sync --target agents-md|cursor [--write <path>] [--check <path>] — the law, in the other tools' vocabularies

Skills and AGENTS.md port between agents; hooks, subagent definitions and plan pipelines do not. These two targets take the half that ports and stop it going stale by hand.

agents-md writes the file every agent on the standard reads and Claude Code does not: the law's rules section (the section matching ledger.lawSection, not the whole file — a copy of the law is a second law), one line per skill taken from that skill's own frontmatter, and one row per glob-scoped rule saying which files it binds to. It is a PROJECTION and never a source: the law is hand-written in the file ledger.law names — LAW.md under D-054, with CLAUDE.md the one line importing it — and this file is regenerated from it, so the two cannot disagree.

cursor writes one .cursor/rules/geonosis-<area>.mdc per .claude/rules/<area>.md, translating the frontmatter: Cursor's globs is ONE comma-separated string rather than a YAML list, and a rule scoped to ** becomes alwaysApply: true with no globs line at all — Cursor ignores globs when alwaysApply is set, and a line that does nothing beside a line that does is how a config starts lying.

geonosis-ledger sync --target agents-md --write AGENTS.md
geonosis-ledger sync --target cursor    --write .cursor/rules
geonosis-ledger sync --target agents-md --check AGENTS.md      # exit 1 on drift

--write names the FILE for agents-md and the DIRECTORY for cursor. --check compares byte for byte and names what differs — including a generated file that is not there at all, which is the drift nobody notices: a rule added upstream and never translated reads exactly like one that was.

Neither target reads the architecture table. They are about the law and the rule files, and demanding an edges table before generating an AGENTS.md would be a coupling nobody asked for.

This repo runs both on itself, checked in tooling/cross-tool-sync.test.ts: a generator with a round-trip test and no repo behind it proves the function, not the files.

Configuration — the ledger block of geonosis.json

Every key is optional and every default is a contract, not a consumer's layout.

{
  "ledger": {
    "plans": "plans",                          // directory
    "progress": "plans/PROGRESS.md",           // the one file a tick appends to
    "proofs": "proofs",                        // directory
    "decisions": "docs/decisions.md",
    "journal": "docs/journal/fallbacks.md",
    "handoff": "HANDOFF.md",
    "architecture": "docs/architecture.md",
    "backlog": "docs/rules-backlog.md",        // the row file burndown reads
    "backlogDone": "\\bDONE\\b",               // this repo's word for a finished row
    "law": "CLAUDE.md",                        // what lawLineCount counts and sync projects
    "lawSection": "^#{2,3} +The laws\\b",       // the section of it AGENTS.md carries
    "agents": "AGENTS.md",                     // where sync --target agents-md writes
    "skills": ".claude/skills",                // read for the one-line summaries
    "rules": ".claude/rules",                  // the glob-scoped rules cursor translates
    "maxLines": 200,                           // captured proof output, bounded
    "commitMessageLines": 4,                   // subject + 3
    "plan": {
      "statusHeader": "^> \\*\\*Status\\*\\*: *(\\S+)",  // group 1: the status word
      "criteriaHeading": "^#{2,3} +Acceptance criteria\\b",
      "criteria": "^- When .+ shall .+",
      "verificationHeading": "^#{2,3} +Verification\\b",
      "after": "\\bafter: *\\[([^\\]]*)\\]",   // group 1: the plans this one comes after
      "done": "^DONE$",                       // which status word means finished
      "plannedAt": "\\bplannedAt: *(\\S+)",    // the revision the plan was written against
      "scope": "\\bscope: *\\[([^\\]]*)\\]",   // the paths the plan is about
      "ships": "\\bships: *(\\S+)"             // `harness`: G0 counts the code in the plan's scope
    },                                         // plus "since": <NNN>, unset by default
    "land": { "branch": [], "prepare": "", "tier": "full", "verify": "geonosis-verify" },
    "runtime": { "include": ["…"], "exclude": ["…"] }
  }
}

An unknown key is an error, not a shrug: a typo in a gate's config is silence.

The plan patterns are regexes because a heading name is a repo's vocabulary

A Workers + D1 app writes ## Done criteria and ## Test plan and ticks a checkbox; this repo writes ## Acceptance criteria and an EARS sentence. Neither is the contract — declaring criteria and declaring how they are verified is. A repo with its own words sets four regexes:

"plan": {
  "statusHeader": "^#{2,3} +Status *$",
  "criteriaHeading": "^#{2,3} +Done criteria *$",
  "criteria": "^- \\[[ x]\\] .+",
  "verificationHeading": "^#{2,3} +Test plan\\b"
}

runtime — G0, generalised

A Medusa storefront's G0 gate ("a tick MUST deliver real runtime code") greps a staged diff for (apps|packages)/.+/src/. That anchor is true of a Medusa storefront and of nothing else: a Workers + D1 app keeps 1,727 of its 2,295 source files outside any src/ directory, so it would refuse every tick that repo has ever made. What travels is the kind of file, so the default is:

"runtime": {
  "include": ["\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|css|scss|sass|less|svg|vue|svelte|astro)$"],
  "exclude": [
    "\\.(?:test|spec)\\.",
    "\\.stories\\.",
    "\\.d\\.ts$",
    "(?:^|/)(?:__tests__|__mocks__|__fixtures__|__snapshots__|__perf__)/",
    "(?:^|/)(?:docs?|tests?|e2e)/",
    "(?:^|/)\\.[^/]+/"
  ]
}

The last exclusion is every dot-directory at once — .shop/, .github/, .claude/ — without naming any repo.

On top of these, tick excludes the ledger's own artefacts: everything under ledger.plans, progress, proofs, journal, decisions, handoff and architecture. A Medusa storefront's G0 excluded ^\.shop/, the directory its ledger lived in; that literal cannot ship, but the fact can, because every one of those files is already a key of this config and so the exclusion follows a repo wherever it keeps them. (Found by running G0 over this repo's history: a test: commit whose whole diff was three files under proofs/ passed, on a .ts proof harness.)

A repo that wants a Medusa storefront's exact strictness keeps its own anchor:

"runtime": { "include": ["(apps|packages)/.+/src/.+\\.(ts|tsx|js|jsx|css|svg)$"] }

Licence

Apache-2.0.