# ratchet > A machine-local observer for AI coding agents. Ingests agent transcripts into > SQLite, prices them with cache-split accounting, detects eight friction > signatures, ranks hotspots, tracks trends across calendar periods, and > promotes persistent shapes into portable findings. Transcripts never leave > the machine. Status: v0.6.12. macOS. 864 tests. atlas v0.1.11. Modelled: Claude Code, Rift, Codex (history back to 2026-03), Grok, Antigravity (durations 100% harness-stated). Captured raw (normalizer pending, honestly BLIND until then): Gemini CLI. Ships with atlas v0.1.11 — a separate, independently-versioned tool on the same installer (see "atlas" below). As of ratchet v0.3.8 `ratchet update` carries BOTH tools; v0.3.7 and earlier moved ratchet alone and said nothing about atlas. As of v0.3.9 the same teaching reaches Claude Code, which reads `CLAUDE.md`, memory, MCP instructions and skills — and never `AGENTS.md`: `ratchet skill --write` installs one user-scope skill per tool, and `ratchet enabled --projects` shows which projects are wired at all. Full agent reference: https://ratchet.daystra.com/RATCHET.md atlas agent reference: https://ratchet.daystra.com/ATLAS.md AGENTS.md paste block: https://ratchet.daystra.com/AGENTS.block.md atlas paste block: https://ratchet.daystra.com/ATLAS.block.md Release history: https://ratchet.daystra.com/CHANGELOG.md Field results: https://ratchet.daystra.com/RESULTS.md ## What problem it solves Coding agents write a complete record of their own work and then never read it. ratchet reads it, so the next agent to open a project can query what went wrong last time instead of rediscovering it — and check whether a fix actually held. ## The agent surface (structured JSON, never prose) Every JSON output is an envelope: `{"meta": {...}, ...}` where `meta` always carries both coverage percentages, the occurrence floor, and caveats as structured objects (`{code, severity, detail}`) you must handle, not skim. Every `query` verb accepts `--json` as a no-op — query output is always JSON. An executor that learned the flag from `trends` / `hotspots` / `grade` / `findings` is routed, not refused. - `ratchet query hotspots [--kind K] [--tool T] [--order seconds|occurrences] [--limit N] [--json]` — every hotspot row for a sweep. Rows carry `confidence`: `measured` (every contributing duration harness-stated) | `derived_upper_bound` (the seconds are elapsed call→result gaps that include permission-approval wait — a ceiling on machine time, never an execution cost) | `unverified` (implausible mean — verify before acting) | `not_measured` (duration is null, not zero). Precedence is worst-first: `not_measured` > `unverified` > `derived_upper_bound` > `measured`. The companion `duration_provenance {harness_stated, derived, unknown}` object carries the full split the scalar had to collapse. Truncation is announced via a TRUNCATED caveat, never silent. - `ratchet query signatures [--kind] [--tool] [--project] [--since D] [--until D]` — raw evidence rows with parsed per-kind `evidence`, anchor timestamps from the action join, and sidechain flags. - `ratchet query trends [--period month|week] [--project P]` — the comparative layer: friction per shape per calendar period, per-period coverage beside every bucket (machine-global; `--project` scopes cells only), and deltas gated per-cell by `comparable` + a structured reason (coverage shift, sparse prior capture, a contributing harness newly visible). Every cell and delta carries `per_1k_in_scope` (null when in-scope is 0); every delta also carries `pct_change_per_1k` and `volume_change_pct`. A newly-visible harness is the `HARNESS_NEWLY_VISIBLE` info caveat, not a blanket refusal. A delta with `comparable: false` is evidence of nothing. `delta_gate` is the remaining blanket and is null when no blanket applies. - `ratchet query coverage` — per-harness in-scope % and coverage %, so you can check comparability yourself. As of v0.6.5 each row also names the single largest record shape it EXCLUDED (`dominant_excluded_shape` / `_count`), so a low in-scope % is readable as "this harness emits a lot of telemetry ratchet does not model" rather than as blindness. `(unclassified)` is the one value that does mean blindness. - `ratchet query who --path P --at T [--window 5m]` — which sessions/commands touched or named a path in a window on this box. Forensics, never now. `STORE_LAG` (ingest is 30-minutely); `WHO_LOWER_BOUND` (only what the tool was handed). Empty-with-caveat, never a bare empty. - `ratchet query sessions [--project P] [--since YYYY-MM-DD]` — per-session cost envelope, one row per `session_id`. Token sums and `cost_usd` are NULL when never measured, never 0. `UNPRICED_PRESENT` when any session has unpriced turns. - `ratchet query intents [--project P] [--since YYYY-MM-DD]` — cost per completed task (the arc intent on `action.intent_id`). One row per `(project, intent_id)`; the outside-any-intent share is a row, never hidden. `INTENT_COVERAGE` names both counts. - `ratchet query skills [--project P]` — Skill invocations per (skill_name, project) over the last 30 days: the USED axis for a skills plan; scope null on purpose, absent = unmeasured, never 0. - `ratchet query orientation [--project P]` / `ratchet grade` — the orientation tax: per-project medians of pre-first-edit navigation intensity (read-only calls, re-search density), censoring disclosed, rework bundled, plus the SPLIT-PROOF working set (distinct files read and total reads per editing session): it counts knowledge loaded, not file layout, so neither splitting nor merging files can fake an improvement. `grade` and `brief` share one attribution pass and so cannot disagree on the same measure. One asymmetry is deliberate: the working-set columns exclude reads of other projects' files, while the pre-edit read-only call counts still include them — a call spent is a call spent, but the file it opened says nothing about THIS project's working set. Never a league table — compare a project against itself over time; TASK_MIX caveat is structural. - `ratchet brief [--json]` — the per-project brief, computed from the store and never authored: a diagnosis, the files this project's editing sessions actually read, the files that get PAGED rather than loaded, the commands that failed here and what worked instead, and the median duration of the slow verbs (median, so slow is not mistaken for hung). `diagnosis.code` is closed: WORKING_SET | SMEARED | PROCESS | LOW | INSUFFICIENT. **INSUFFICIENT is a refusal, and it is a feature** — under the 5-session floor no diagnosis is offered at all, because a classification is a verdict. The evidence is still shown; the verdict is withheld. A typo'd project gives an empty brief with a BRIEF_NO_SESSIONS caveat, never a zero. - `ratchet findings [--json] [--limit N] [--all]` — the fleet-portable tier (50 rows by default since v0.6.9; a `TRUNCATED` caveat names the total): claims that persisted across consecutive sweeps, privacy-gated (no paths, no machine ids, no MCP server segments). `ratchet findings contest --reason "..."` marks a claim disproved; it is frozen, never deleted or re-asserted. - `ratchet hotspots --json`, `ratchet report --json`, `ratchet doctor --json` — envelope forms of the classic surfaces; doctor adds a structural `faults` array matching its exit code. - v0.6.9: a read-only verb pointed at a path with no store refuses with `{"error":"no_store"}` and **exit 3** — it never creates one; only `init`, `run` and `ingest` do. `doctor` faults `NEW_BUT_RAN` when two or more ingest runs have captured nothing (the scheduler works; discovery finds no transcripts). `ratchet normalize --rescan` resets both cursors and re-examines every raw record (idempotent) — the hand-SQL recipe is retired. `--json` on `query ` is a no-op, never a refusal. - v0.6.10: `doctor` reads the scheduler's last exit code and faults `SCHEDULE_LAST_EXIT` on non-zero (a loaded job is not a running one — a launchd job that exited 78 on every run for two hours read healthy on 2026-09-05); `wire`/`update` re-bootstrap the job whenever the installed binary's bytes change (sidecar hash in `~/.ratchet/schedule.sha256`); `update` runs the wire step with the NEW binary. **The 0.6.9→0.6.10 update itself is wired by 0.6.9: run `ratchet wire` once after it, then every update self-heals.** - v0.6.11: `ratchet query skills [--project P]` and the MCP tool `ratchet_query_skills` (15th tool) — Skill invocations per (skill_name, project) over the last 30 days, the USED axis for praxis's skills plans; `scope` is null on purpose (`SCOPE_NOT_CAPTURED`), absent means unmeasured, never 0. Ships atlas v0.1.10: `atlas --json` and `atlas lookup --json` emit the MCP tool's exact record, and `--no-build` travels through so a hook fails open in milliseconds on a tree with no manifest. - v0.6.12: the SessionStart packet's `(n of N)` markers name each section's TRUE total — `brief` carries `working_set_total` / `paging_files_total` / `traps_total` / `slow_commands_total` (trap and slow null on an unswept store); before, N was the brief's own cap (15/15/8/6), so "(5 of 15)" stood for 97. MCP `ratchet_query_hotspots` defaults to `order: occurrences` like the CLI; MCP `kind` is refused unless it is one of the eight kinds (hotspots, signatures, trends); MCP `ratchet_findings` refuses a negative `limit`; wiring faults say `Fix: ratchet wire` (there is no `ratchet install`). Ships atlas v0.1.11: packaging only, code byte-identical to v0.1.10 — release tarballs no longer embed macOS extended attributes (`tar --no-xattrs`), whose per-process `com.apple.provenance` value made identical source hash differently depending on which session packaged it. ## The `` packet If a `` block appears at the top of a session, `ratchet hook orient` put it there via a user-scope `SessionStart` entry in `~/.claude/settings.json`. It is this project's measured working set, paged files, traps, real median command times, orientation baseline, and atlas entry points / import hubs. - **Computed, never authored.** Both halves are read from artifacts something else already produced (the baked `ratchet brief` heat row, the atlas manifest); the hook creates neither, so the packet cannot drift from what happened and cannot be forgotten to be updated. - **Bounded.** ~2 KB — a 3,200-byte ceiling, ~800 tokens, with each section capped individually. Over the ceiling it drops whole lines, cheapest first, never a mid-line cut. - **Silent when there is nothing measured to say.** No root, no manifest, no baked heat → nothing is emitted and the exit code is 0. Silence there means NO DATA, not no friction, and an unmeasurable quantity says so rather than rendering as an empty list. - **Fail silent on a deadline** (1,500 ms in-process, 5 s at the harness): a session start is a human waiting. Treat it as the answer to "what will I probably need", not as a map of the code. `ratchet brief ` is the unabridged version. Full detail: https://ratchet.daystra.com/RATCHET.md ## MCP `ratchet mcp-server` serves the same envelopes over MCP stdio: tools `ratchet_context`, `ratchet_orient` (v0.4.0 — the fused heat + atlas-map packet for one cwd, the same bytes the SessionStart hook injects, and the one orientation door that works before any sweep exists), `ratchet_query_hotspots`, `ratchet_query_signatures`, `ratchet_query_trends`, `ratchet_query_coverage`, `ratchet_query_background`, `ratchet_query_who`, `ratchet_query_sessions`, `ratchet_query_intents`, `ratchet_query_orientation`, `ratchet_brief`, `ratchet_findings`, `ratchet_status` (v0.4.0 — the WHOLE of `doctor --json`: pipeline health AND the install/atlas wiring sections, `faults` the flat union of all three). Register the binary in your MCP client config. `ratchet_context` returns TWO rankings of the same rows: `top_hotspots_by_count` (read this first — occurrences are measured for every detector and are the fleet-portable rank) and `top_hotspots_by_time`. The time table is never removed, because an absent table would read as "no time cost anywhere" — but wherever TIME_RANK_DERIVED fires it is an upper-bound ordering. REPEAT, PAGING, SERIAL, REPHRASE, CEREMONY, BYPASS and TRAIN measure no duration at all, so they cannot appear in a time ranking however large they are. ## atlas `curl -fsSL https://ratchet.daystra.com/install.sh | sh` installs TWO tools. The second is **atlas v0.1.10** — a stdlib-only, single-file manifest of pointers for whatever codebase you are standing in: declarations, name substrings, file paths, path routes, endpoints, config keys, module registry. It lands at `~/.ratchet/bin/atlas` and registers its own MCP server (`atlas`, one tool: `atlas_lookup(query, path?)`). **It is not a faster grep, and field measurement says so plainly: atlas is SLOWER per lookup** (16 `rg` ≈ 175 ms vs 16 atlas ≈ 3 s) **and costs more tokens than an empty `rg` when the answer is "nothing"** — deliberately, since an empty result with a `searched`/`not_modelled` footer is evidence where an empty grep is a shrug. It pays by deleting the refine loop: the 2–4 re-worded greps and 8–15 speculative file reads that follow a partial answer. Numbers, including what it does *not* save, are in ATLAS.md — kept in one place so they cannot drift. **Independently versioned, on purpose.** atlas moved three times on 2026-08-03 while ratchet moved once; a shared number would force a ratchet release per atlas fix and let a ratchet release silently claim an atlas change. `latest.json` therefore names both — `version` for ratchet and `atlas_version` / `atlas_artifact` / `atlas_checksums` for atlas — each in its own write-once directory (`/v/`, `/atlas//`), each with its own checksums file. Pin either side alone: `VERSION=`, `ATLAS_VERSION=`. Opt out with `RATCHET_NO_ATLAS=1`. **The installer's menu (v0.3.9).** Four things beyond the binaries and the schedule are independently choosable and independently idempotent. The ones that touch only ratchet's own install stay OPT-OUT — `RATCHET_NO_MCP`, `RATCHET_NO_SCHEDULE`, `RATCHET_NO_ATLAS`, `RATCHET_NO_PATH`. The three that write into files the installer did not create are OPT-IN, and the asymmetry is deliberate: a `curl | sh` may register its own MCP server, it may not silently edit somebody's repositories or their `~/.claude`. RATCHET_SKILL=1 the user-scope skills (~/.claude/skills) RATCHET_HOOK=1 the SessionStart hook (~/.claude/settings.json) RATCHET_BLOCKS=1 the blocks, in every measured project root RATCHET_BLOCKS_FILE=CLAUDE.md which file the blocks go in RATCHET_ALL=1 all three at once There are no prompts: the script is piped from stdin and has no TTY, so the menu is PRINTED with each component's state and the variable that flips it. Every optional step probes its subcommand (`ratchet skill --help`) before invoking it, so a pinned older `VERSION=` reports "this build cannot" rather than a failed install. **Upgrading:** `ratchet update` (v0.3.8+) reads both halves of that manifest and moves each tool on its own version line. Re-running `install.sh` does the same and is safe to re-run: the PATH append is marker-guarded and skipped outright when `$PATH` already contains the bin directory, the MCP splice and the launchd scheduling both write nothing when they already match. On ratchet v0.3.7 and earlier, `ratchet update` moved ratchet alone and never mentioned atlas — so an update-only machine is still on its install-day atlas until it runs `update` once more from v0.3.8. What it is for is the SHAPE of the answer, not the speed of it: `atlas ` takes no verb and no setup, every answer carries `file:line` verified against disk at answer time (a pointer that moved says so in its own `note`), an empty result means empty (the live tree is grepped before one is reported) and every answer ends with a footer naming what was searched, what was not, and what this build does not model. `NOT-INDEXED` is a separate verdict meaning *absence here is not absence in the code*. Three disclosures shipped in atlas 0.1.3, all field-driven: - **A thin answer is gated like an empty one.** The live grep runs whenever no index holds the term ITSELF — a neighbour like `idle_timeout_triggers` never stands in for `idle_timeout`. Every answer carries `exact_indexes` (the doors that held the exact term you typed); when it is empty, `neighbours_only` is true and a one-sentence `qualifier` says everything above merely CONTAINS what you asked for. - **Rust enum variants are declarations.** `DaemonCommand::CaptureNow` resolves qualified (through the parent) and bare (`CaptureNow`), as kind `variant` — the variant name is the handle a reader of fleet Rust actually arrives with. - **STALE_BINARY names a stale server.** A long-lived atlas MCP server re-stats its own file per call and warns when atlas itself was replaced on disk underneath it; the fix is a SESSION restart (registrations are fixed at session spawn), and the notice says so. The complete surface: - `atlas ` — THE DOOR. Any word: symbol, topic, endpoint, config key, path, literal. Routes across declarations, name substrings, file paths, path routes, endpoints, config keys and the module registry. - Verbs, for when you already know the SHAPE of the answer: `where ` (defined), `refs ` (used — this is the live grep that `NOT SEARCHED code bodies` points you at), `file `, `config `, `wire `, `stale [--fix]` (per-POINTER drift: VALID / MOVED / INVALID, and `--fix` re-anchors without a rebuild), `stats`, `build [root]`, `mcp-server`. - `--also ` searches sibling manifests — the answer to "I need a dependency's symbols" that does not involve indexing node_modules. - **The manifest is self-building and self-refreshing.** It lives out of tree at `~/Library/Caches/ratchet-map/`, keyed by ABSOLUTE root (a basename is not an id — on this machine `public` named six unrelated projects), and a stat-only freshness walk rebuilds it when the tree has moved on. Agent furniture (`.claude`, `.codex`, `.rift`, `.ratchet`, `.cursor`, `.arc`, and agent scratch dirs) and installed dependencies are never indexed — one shared predicate covers the walk and `refs`' live grep, so the index and the fallback cannot disagree. Automatic builds are capped at 2,000 files; `atlas build ` has no cap because a human is watching it. Notices ride in `meta.notices`: BUILT / REBUILT (info), STALE_BUT_OVER_CEILING (warning — you are reading an OLD manifest), NO_ROOT (error — NOTHING was searched). - **`--db PATH` is an override atlas never auto-writes.** A manifest you named belongs to you: it will not be created and will not be rebuilt underneath you. `--no-build` asks for the same contract on the derived path. Self-maintenance is a property of the door that takes no flags. - MCP: one tool, `atlas_lookup(query, path?)`. Not eight — a verb menu in a tool list rebuilds the routing tax the one-door form deletes. **`path` exists for the working-directory trap:** a user-scope MCP server inherits the cwd of the CLIENT PROCESS that spawned it — the directory `claude` was launched from, not your project. Measured live: a session launched from `$HOME` returned `no_root` for every call, correctly reporting that it had searched nothing while the real project sat two directories away. Pass `path` (any absolute path inside the project you mean) whenever those could differ, and always after NO_ROOT. - One CLI quirk: a query starting with `-` cannot be passed bare — argparse claims it as a flag. Use `atlas lookup -- -webkit-mask` (the `--` must follow the verb), or the MCP tool, where the query is a string field and no quoting rule applies. Full agent reference: https://ratchet.daystra.com/ATLAS.md Paste block: https://ratchet.daystra.com/ATLAS.block.md ## Lifecycle — one command each, and nothing hand-typed - `ratchet run` — ingest, normalize, price, sweep, hotspots, findings, and bake the orient heat rows. What the scheduled job runs every 30 minutes. - `ratchet report [--markdown]` — the verification surface: every number an agent can cite renders here from the same structs, including Trends and Findings sections. - `ratchet trends [--markdown]` — the comparative report. - `ratchet doctor [--json]` — whole-install verification, not just pipeline health: is the DATA trustworthy AND is this machine WIRED. Before it existed, three of the four install steps were silent when skipped and doctor called a machine healthy where the MCP server had never been registered. Pipeline faults: `STALE`, `UNPRICED`, `LOW_COVERAGE`, `UNMEASURED_HARNESS`. A store that has captured NOTHING is **NEW, not STALE**, and is not a fault at all: it reports `state: NEW`, its `coverage_pct` is `null` rather than `0.0`, and it names `ratchet run`. Nothing has been lost when nothing was ever there, and a fresh install must be able to exit 0 on its own health check. Install faults: `BINARY_MISSING`, `MCP_UNREGISTERED`, `MCP_STALE_PATH`, `LAUNCHD_UNLOADED`, `LAUNCHD_STALE_PATH`, `STORE_UNMIGRATED`, `STALE_BINARY` (installed binary OLDER than the build that last wrote the store — only that direction faults). atlas reuses three of those words prefixed `ATLAS_`. One flat `faults` array that always matches the exit code; every fault printed before it exits once. - `ratchet wire [--no-mcp] [--no-schedule]` — register both MCP servers and schedule the job, idempotently. **Everything already correct writes nothing at all** — no config rewrite, no bootout/bootstrap window. `~/.claude.json` is spliced as raw text and verified to parse and deep-equal the original with only that entry changed. atlas is reported `absent` rather than registered when its binary is missing. - `ratchet update [--check] [--no-rewire] [--allow-downgrade]` — self-update of **BOTH tools** from the one `latest.json` the installer reads, SHA-256 verified in-process (not by shelling to `shasum`, which an earlier `$PATH` entry could defeat), then RE-WIRES, because an upgrade that leaves the wiring behind is how drift starts. One line reports both outcomes, always: `ratchet updated: 0.3.7 -> 0.3.8; atlas updated: 0.1.0 -> 0.1.1`. **It refuses to DOWNGRADE, per tool independently** — the two version separately, so one can legitimately be newer while the other is not. A missing checksum entry is a hard failure for either artifact, never a skip; an atlas absent from the manifest is reported as absent, which is a different branch. `--check` touches nothing at all and still reports both. **v0.3.7 and earlier updated ratchet ONLY** — silently, with no mention of atlas — so a machine that has only ever run `ratchet update` still carries whatever atlas was current on its install day. One more `ratchet update` (on v0.3.8+) or a re-run of `install.sh` catches it up. - `ratchet agents-block [--write PATH] [--all] [--dry-run]` — emits BOTH blocks, each between its own delimiter pair (`ratchet:start`/`end`, `atlas:start`/`end`), so the two update independently. `--write` edits in place: re-running updates, never duplicates, and never touches a byte outside the delimiters. **`--all` writes into every project the store has measured an editing session for** (v0.3.9), at the tracked root it lives at now — the root is re-derived from the session cwds and re-checked for a `.arc`/`.git`/`.hg` marker immediately before the write, so a project whose tree moved or was deleted is REPORTED and skipped rather than guessed at. With `--all` the argument is a bare file NAME joined to each root (`--write CLAUDE.md --all`); a path is refused. One project's unbalanced marker pair does not stop the others — it is reported and the exit code carries it. - `ratchet skill [--write] [--remove] [--dry-run] [--skills-dir DIR]` (v0.3.9) — the surface `agents-block` cannot reach. Claude Code reads `CLAUDE.md`, memory, MCP instructions and skills; it never reads `AGENTS.md`. Measured on the reference machine: 42 tracked projects, 22 with an `AGENTS.md`, 8 with a `CLAUDE.md`, and for that runtime the block command's enablement was 0 of 42. This writes ONE user-scope skill per tool into `~/.claude/skills//`, which covers every project at once and creates nothing inside any repository. Two skills, not one: the tools version independently, and a skill's `description` is an INVOCATION TRIGGER — "where does this symbol live" and "has this shape cost time before" must not share one. The body is the same `include_str!`'d bytes the `AGENTS.md` block carries, so the two cannot drift. A file at that path without ratchet's marker is REFUSED, never overwritten and never removed; `--remove` takes the file and its emptied directory and leaves nothing behind. - `ratchet enabled [--projects] [--json]` (v0.3.9) — ENABLEMENT, not health (`doctor` owns health and the exit code). Machine-wide: both binaries and their versions, both MCP registrations, the SessionStart hook, the two skills. With `--projects`, one row per project the store has measured: editing sessions, whether its orient heat is baked and still servable, whether atlas has a manifest for it, and which blocks are current/stale/ absent/refusing in `AGENTS.md` AND in `CLAUDE.md`. Projects with no tracked root are named rather than dropped. **Never exits non-zero** — an un-enabled project is a normal state, not a fault. - `ratchet remove [--purge] [--dry-run] [--yes]` — lists every part with its size before the confirmation gate, both MCP registrations included; the bin directory is sized once with both binaries named, because the number above a prompt must be the number that gets deleted. **The store is KEPT unless `--purge`** — it is the only copy of a corpus whose sources self-delete on a rolling window. `--purge` (v0.4.0) also takes atlas's manifest cache in `~/Library/Caches/ratchet-map` — the one part of the install outside `~/.ratchet`; reaching into `~/Library/Caches` costs a flag. - `ratchet hook install|orient|bake` — the `SessionStart` orientation hook. `install` writes the entry into `~/.claude/settings.json` at USER scope (so it applies in every project, no per-project step); `--remove` unwires it, `--dry-run` prints the change, `--settings PATH` targets a fixture. `orient [--cwd D] [--timeout-ms N] [--debug]` emits the packet — **always exit 0, never writes anything**, prints nothing on timeout or error, because a hook that hangs or shouts at a session start is worse than no hook. `bake [--project]` recomputes the heat rows `orient` serves; `ratchet run` already does it on every tick. ## The six friction signatures - WAIT — a call far above its own tool's median duration (Agent calls are excluded: a subagent session is a machine working, not waiting). - REPEAT — the same REGION of a file read again with no edit between — true redundancy (compaction-aware: a re-read after a context-compaction boundary is rehydration, not friction). - PAGING — DIFFERENT regions of one file read in sequence: the file is too large to hold in a single read. A property of the file, not the agent — the fix is an index or a split, never "remember harder". Separated from REPEAT by comparing stored read digests; it was 37% of REPEAT's count before the split existed. - REPHRASE — consecutive searches with no edit between them. - SERIAL — long runs of single-call read-only turns that never got batched. - CEREMONY — a failure followed by the same command retried with a tweak. - BYPASS — a hand-rolled `sqlite3` against a substrate a registered tool already serves (arc, ratchet, kyber). Count-only; the shape says whether the session also used that tool's own surface (a coverage gap) or never did. - TRAIN — three or more consecutive same-tool same-stem calls in one session (a Bash verb or an MCP tool): one call per turn that had one shape. Count-only, one signature per (session, tool, stem); shape `identical` (run it once, or arm a watcher) / `varying` (a loop or a batch) / `target_not_captured`. Read/Edit trains are excluded — PAGING and REPEAT own them. ## Data model — three tiers - `raw_record` — transcripts, verbatim. Never leaves the machine. - `action` — normalized and priced. Never leaves the machine. - `finding` — derived shapes only, privacy-gated at write time. Portable by design; the only tier that ever could leave. ## Accounting rules that matter when reading its output - Token buckets (input / cache_read / cache_write / output) are priced separately. Collapsing them inflated the reference corpus 6.7x. - NULL means "not measured", never 0 — for costs, durations, tokens, and ratios (a delta against a zero prior is `null`, not 0%). - A bundled-subscription harness prices NULL with `action.cost_basis = 'subscription'` — the dollar question does not apply, which is not the same as free. 0 would make every dollar-ranked routing comparison favour it by construction. - Rates are date-keyed and immutable: an action prices at the rate in force when it happened. - A project is a REPOSITORY root (`.arc`/`.git`/`.hg`), and each action is attributed by the path it touched, not by its session's cwd. Files are keyed repo-relative, so a worktree's rows join their base project instead of fragmenting the file's coverage. An unresolvable path is NULL, never the session's project. - Time is machine-local; occurrences and ranks are portable. Findings carry counts and ranks, never seconds. ## What ratchet does NOT measure **ratchet measures time lost, never coverage of the work.** Every one of the six detectors reads the SHAPE of tool use — reads repeated, searches re-worded, calls issued serially, steps that serve the tool, gaps between call and result. None of them inspects what was produced. It never sees the requirement and never sees what was not touched, so completeness is outside the instrument, not merely absent from it. A session that did nothing and a session that did everything correctly both emit **zero signatures**, and this build cannot tell them apart. So a clean result is not evidence of completeness — and it is not even evidence of low friction, because a missing signature can mean *not-measured* rather than *measured-and-none*: `BLIND_HARNESS` (no normalizer), `PRE_INSTRUMENTATION` (records predate the gate), or simply below the occurrence floor in `meta.floor`. The honest reading of a clean sweep is narrow and exact: > **This build found no friction it can model in what it could read.** **Do not use ratchet as a quality gate or a done-check.** It is a friction meter, and its silence is not an all-clear. ### `coverage_pct` is the one to watch It is coverage of the **ingestion** — measured ÷ in-scope records — never coverage of your task. `coverage_pct: 100.0` means normalization succeeded on everything it could model. It says nothing about whether anything was missed. That misreading happens on the *healthy* path, where no caveat can fire, so the fix is a field rather than a warning: **`meta.counts` ships the `captured` /`in_scope` / `measured` records both percentages are derived from.** Audit the denominator instead of trusting the quotient — the same reason `stated_occurrences`/`derived_occurrences` ride beside `confidence`, and `dominant_excluded_shape` beside `in_scope_pct`. ## How to read the output honestly Respect the structured caveats — they are fields, not decoration. Their `severity` is a rule: **`warning` means an action exists** (write a normalizer, run `normalize`, distrust these seconds); **`info` means the claim is bounded and there is nothing to do**. A condition nothing can change is filed as info deliberately — a warning that can never clear is an alarm people learn to ignore. - `confidence: "unverified"` — an implausible mean (>15 min/call), almost certainly a gap where the human walked away. Verify before acting. - `confidence: "derived_upper_bound"` — plausible, but the seconds were timed by the elapsed call→result gap, which includes however long a permission prompt sat unanswered. Read it as a ceiling on machine time, never as execution cost. On this build's reference store only 113 of 51,954 timed calls are harness-stated. - DERIVED_DURATION — the same fact about the STORE: what fraction of all timed calls carry no harness-stated duration. - TIME_RANK_DERIVED — the same fact about the ROWS IN FRONT OF YOU, naming the top one. A rank ordered by derived seconds is an upper-bound rank. - TIME_NOT_MEASURED — a count-led view legitimately mixes shapes that time themselves with shapes that never do; this names which. Their seconds are ABSENT, not zero, and cannot be compared against WAIT rows. - BLIND_HARNESS (warning) — captured, 0% in scope, and this build has NO normalizer for that harness. Absence from rankings is not zero friction, and the fix is a normalizer, not a re-run. - PRE_INSTRUMENTATION (info) — captured, 0% in scope, but a normalizer DOES exist; the records predate the instrumentation it requires. Unmeasurable by design, not a gap and not a broken normalizer. Splitting this out of BLIND_HARNESS matters: conflated, it told readers a harness was unreadable when the build reads that harness fine. - FOREIGN_READS_EXCLUDED / AGENT_FURNITURE_EXCLUDED (info) — what the working-set attribution pass dropped: files outside the project, and the agent's own config/memory/scratchpads. `grade` and `brief` share the pass, so they share the numbers AND the sentence describing them. - `meta.floor` — how many shapes/signatures the 3-occurrence floor dropped. Invisible, not absent. - SINGLE_MACHINE — generalize the SHAPE, never the specific verb. - A trends delta is only meaningful when `comparable: true`; the reason field tells you exactly why when it is not (a month where ingest was down must not read as a month friction dropped). Gates are per-cell; a newly-visible harness is `HARNESS_NEWLY_VISIBLE`, not a blanket refusal. Read `pct_change_per_1k` when `VOLUME_SHIFT` fires. `delta_gate` null means "no blanket", not "no trends". ## Privacy No service, no account, no telemetry. Everything is a local SQLite file.