Release 0.6.0 - #377
Merged
Merged
Conversation
A review scored this project's direct effect on AI code entropy at 7.1 and asked whether it should detect what an agent actually does to a codebase: comments that restate the code, an abstraction used once, a function's responsibility widened, a compatibility branch protecting a caller that should have moved. The list is accurate and this repository does nothing about it. The question was never whether those failures matter -- it was whether they belong here, and the answer had been neither promised nor refused. That is the state that lets a claim drift in later, when somebody writes a launch post. Those checks read a diff. Everything here reads a record store. They share a motivation and no mechanism: judging whether an abstraction is premature needs taste about code, and saying "this was ruled out in March and the reason still holds" needs provenance, lifecycle and a trust boundary. Building the first inside the second gives one name to two data models. ADR-0013 already made this argument about a dashboard, an embedding layer and an organisation graph, and it recorded the measurements that matter here too: three registered runs found no significant effect from injecting more context. The value is control, not volume -- which records may be obeyed and which are stale. Entropy review bets on the hypothesis this project's own data declines to support. So it is out of scope, and the README keeps the distinction it already draws: this does not slow the rate an agent produces entropy, it stops that entropy being inherited by the next agent as settled design. The second sentence is what the lifecycle and the path scope do. The first is one this repository cannot support, and a future review scoring it low is describing the product correctly rather than finding a gap. Record-Id: r-entropyscope Limit: an unclaimed capability is indistinguishable from an unbuilt one, so a scope question left silent gets resolved later in whichever direction sells better Ruled-out: Shipping it as an optional skill in this repository | optional does not separate it -- the skill carries this product's name and plugin, and its failures read as this tool's failures Ruled-out: Deferring it to a later milestone | a deferral is a promise with no date, and this question has now been raised in two reviews Ruled-out: Leaving the README silent on it | silence is what produced the question twice Certainty: firm Blast: local Undo: easy Verified: readme, manifest and compatibility-matrix suites pass with the ADR added; the README and docs/COMPATIBILITY.md both already decline to claim entropy suppression, which is the state this decision holds Unverified: nothing executable changed
Both were found by a production audit of the released tag, and both were reproduced before being fixed. The GitHub Actions ran `npx --yes commitlore` whenever `cli-path` was empty, which is the default and which neither action's usage block mentions. This package is `"private": true` and has never been published -- ADR-0011 makes distribution a git clone, not a registry -- so `commitlore` on npm is an unclaimed name returning 404 to this day. The fallback could not succeed legitimately and could succeed for whoever registers it first, inside a workflow holding the caller's token, with the workspace checked out and its `.git/config` carrying an authorization header. It survived review because nothing exercises it: this repository's own workflows always pass `cli-path`, and every test sets the environment override. The one branch nobody runs was the one an outside adopter always takes. There is no fallback now. `cli-path` is required, and an action that cannot find its CLI says so instead of reaching for a public registry. `commitlore_before_change` returned the trailers of a record it had just labelled `blocked`. A record is graded blocked when its content matched an injection pattern, and this server's own instruction text tells the model that blocked means the content was withheld. `inject` withholds it and `commitlore_query` withholds it; this route labelled the payload and handed it over, and a model reading one tool has no way to know three surfaces disagreed about one record. Withheld at the query boundary rather than in the caller, so a future route cannot reintroduce it by forgetting to wrap. The record still appears -- silence would be worse than the leak, because an agent told "no records" edits as if nothing was ever recorded about the path. Record-Id: r-actionsleak Limit: a code path that no test and no first-party workflow exercises is the one an outside adopter takes by default, and its absence from CI is not evidence it is unused Limit: a trust label the caller must act on is worthless unless the value it describes is actually withheld at the point the data is built Ruled-out: Publishing a placeholder package to claim the npm name | it makes a registry entry the product does not use, and the fix is to stop reaching for a registry rather than to own one more artefact Ruled-out: Keeping the fallback and pinning a version | there is no published version to pin, so this would have been a slower way to fail Ruled-out: Wrapping withholdBlocked at the MCP handler | the same omission would recur at the next route; the boundary that builds the data is where it belongs Certainty: firm Blast: system Undo: easy Verified: registry.npmjs.org/commitlore returns 404 and package.json is private, so no legitimate resolution existed; both action manifests defaulted cli-path to empty and both runners took the npx branch; no executable npx reference remains and both scripts parse; a blocked record's payload no longer appears anywhere in the before-change response while the record itself still does, and an ordinary record is unaffected; before-change, mcp, action-lint, action-preserve and query suites pass at 248; typecheck clean and dist rebuilt Unverified: whether any third party adopted these actions without cli-path, which cannot be determined from here
`doctor --fix` writes the notes refspec and fetches nothing. Availability depended on that refspec, so the remedy this tool prescribes flipped an honest `unfetched` into a confident `absent` while the upstream records stayed exactly as invisible. The warning went away; the cause did not. That is worse than an ordinary wrong answer. This module's own comment says a false empty is the single most dangerous answer the tool can give, because an agent reads it as nothing was ruled out and nothing is off limits -- and `absent` is the state that promises the empty is true. A remote plus no local notes ref is `unfetched` now, full stop. Whether a refspec covers notes still decides whether a fetch would help, which `doctor` still reports; it never decided whether an empty answer could be trusted, and it no longer pretends to. Record-Id: r-refspecfetch Limit: configuring a refspec is not fetching through it, and a state machine that conflates the two turns its own remedy into a way of hiding the problem Ruled-out: Making doctor --fix run the fetch | it reads git config and touches no network by design, and a diagnostic that silently reaches out is a different tool Ruled-out: Adding a fourth state for configured-but-unfetched | the existing vocabulary already answers the only question callers ask, which is whether an empty result can be trusted Certainty: firm Blast: module Undo: easy Verified: reproduced end to end -- a fresh clone of an upstream holding a notes-only record reports unfetched, doctor --fix writes the refspec, and the state stays unfetched where it previously became absent with the record still invisible; notes, doctor and query suites pass at 196 plus the two pinned regressions; typecheck clean Unverified: nothing else -- the refspec check keeps its diagnostic role unchanged
The "With CommitLore" block was invented. `Must respect` and `Do not retry without new evidence` appear nowhere in this codebase -- the projection prints `Limit`, `Ruled-out`, `Warn` and `Other` -- and the block was introduced under the sentence "Before editing, the agent receives:", which is a claim about behaviour rather than an illustration. Four languages, and it shipped in v0.5.1. Replaced with the actual output of `inject --path`, captured from a scratch repository holding exactly the record the scene describes. The trust marker comes with it, because it is the part of the real output a reader most needs and the invented block had no equivalent for: `[claim]` says this record is information rather than an instruction, and only a trusted author's record renders as `[directive]`. The uninstall section promised more than the command delivers. It said the command names "per-repository hooks", plural, and pointed at `commitlore hooks uninstall` -- which removes `commit-msg` and leaves the `prepare-commit-msg` and `post-commit` hooks `init` also installs. Verified: three hooks after `init`, one removed, two remaining. The sentence now says which one, and which two it does not. Record-Id: r-realoutput Limit: a README block introduced as what the tool prints is a behavioural claim, and inventing its shape is the same defect as inventing a number Ruled-out: Keeping the invented labels and marking the block an illustration | it was introduced as what the agent receives, and a reader has no way to tell an illustration from a transcript once the framing says receives Ruled-out: Fixing hooks uninstall to remove all three instead | that is a behaviour change with its own failure modes -- the two remaining hooks fail closed -- and it belongs in a ticket rather than beside a documentation correction Certainty: firm Blast: local Undo: easy Verified: neither invented label appears in src/, and the replacement is the byte output of inject --path against a repository carrying that record; init installs three hooks and hooks uninstall removes one, reproduced; readme, readme-order, readme-numbers, compatibility-matrix and install-script suites pass at 92 across four languages; check-readme-numbers exits 0 Unverified: nothing executable changed
ADR-0020 §3 ordered one sentence removed from the product surface and called it the most dangerous one there: An empty `matched` array means the check ran and found nothing -- it is a verdict, not an absence. It was removed from `commitlore_guard` and left on `commitlore_before_change`, which is the surface a model actually reads before it edits. At 22% recall a miss is the common case, so silence is not a safety result, and the tool most likely to be consulted was the one still calling it a verdict. The test that should have caught it looked at one name. It looks at every tool that returns guard matches now, which is the shape the ADR asked for -- "every surface that exposes guard states its measured limits" -- rather than the one surface whose name contains the word. Record-Id: r-guarddisclose Limit: a disclosure asserted by tool name covers the tool that is named, and the ADR's requirement is about every surface that exposes the behaviour Ruled-out: Removing the sentence without adding the measured figures | the ADR requires both, and an empty description discloses nothing about a 22% recall Ruled-out: Asserting the disclosure by scanning the whole tools array | a new tool that exposes guard should have to be added to the list deliberately, so the omission is visible in a diff Certainty: firm Blast: module Undo: easy Verified: the banned sentence appears nowhere in src/; both guard-exposing tools now carry the measured precision, recall and the non-guarantee; the mcp suite passes at 44 against the real stdio server; typecheck clean and dist rebuilt Unverified: the other tools' descriptions, which do not expose guard matches
Decide that entropy review is a different product
My first attempt at this made `notesAvailability` ignore the refspec, so a remote plus no local notes ref always reported `unfetched`. Two tests caught what that costs, and they were right. A refspec added after cloning is indistinguishable, from config alone, from one that was fetched through and found nothing. The shallow-history fixture is exactly that shape -- refspec added post-clone, upstream genuinely holding no notes -- and the broad change turned it `incomplete`, which moves `guard`'s exit code. That is a false alarm on every repository configured after the fact, traded for a narrow wrong answer. The defect is real and it is not in the state machine. `doctor --fix` writes the refspec, fetches nothing, and then prints `ok` with `fixed by --fix` -- which is the last thing an operator reads before believing the mirror is sorted, while every query still answers from a mirror that was never retrieved. The configuration is right and the records are still missing. So the check says so: after `--fix` it reports the ref is now covered and that nothing has been fetched through it yet, and prescribes `git fetch`. No state changes, no exit code moves, and the sentence that used to read as "repaired" now reads as "half done". The rejected approach is recorded on the function, because the next person to read that comparison will have the same idea. Record-Id: r-fetchowed Limit: config alone cannot separate a refspec that was fetched through from one that was only written, so the availability verdict cannot carry that distinction Ruled-out: Making notesAvailability ignore the refspec | it fires on every repository configured after cloning and flips guard's exit code through the incomplete flag, which is a broader harm than the narrow one it fixes Ruled-out: Having doctor --fix run the fetch | it writes reversible local config by design and touches no network, and a diagnostic that silently reaches out is a different tool Certainty: firm Blast: local Undo: easy Verified: doctor --fix on a fresh clone now reports the ref covered, that nothing has been fetched through it yet, and prescribes git fetch origin; doctor, notes, notes-availability, shallow-history and query suites pass at 210 with the original availability behaviour restored; typecheck clean and dist rebuilt Unverified: whether an operator reads the second clause, which is the part no test can hold
Close what a production audit found in the released tag
…t carry it The README leads with the plugin and then, sixty lines later, tells the reader to run `commitlore init` and `commitlore context .`. Nothing between those two points puts a `commitlore` anywhere. `package.json` is private with no `bin`, `.claude-plugin/plugin.json` declares no executable, and the plugin's payload is `.mcp.json`, `hooks/hooks.json`, `skills/` and `dist/` -- so a reader who took the headline path and typed the documented command got exit 127. The MCP server offers no route around it: seven tools, none of them `init`. Nothing in the product was wrong. The plugin does what its own section claims, the hook resolves the bundle through `CLAUDE_PLUGIN_ROOT` without needing `PATH`, and `docs/COMPATIBILITY.md` has said all of this precisely for some time -- it already separates what a path requires from what it checks, and already records that the plugin path enforces nothing. The README simply never inherited that care. So the correction is one sentence per language saying what the plugin holds and where the CLI comes from, phrased to stay consistent with the document that is authoritative on it rather than to restate it. The setup skill had the same gap and is worse placed to survive it: it is shipped by the plugin, so its reader is by construction the person least likely to have the CLI. It now names the CLI's origin before the first command, and gives the bundle path as the fallback the hook itself uses. Record-Id: r-pluginpath353 Limit: a plugin manifest has no way to add anything to PATH, so no plugin-side change can make the documented commands resolve Ruled-out: Wiring scripts/commitlore-bootstrap.sh into the plugin | it npm installs an unclaimed registry name into the plugin data directory, which was closed as a supply-chain hole, and re-arming it to make a README sentence true trades a documentation defect for an execution one Ruled-out: Adding a bin entry to package.json | the package is private and publishes nothing, so the entry would name an install path no user can take Ruled-out: Rewriting the later sections to drop the bare commitlore form | three skills and both quickstart blocks use it, the CLI is real for anyone who ran the installer, and rewriting a correct command is a larger edit than naming its source once Certainty: firm Blast: local Undo: easy Verified: package.json is private with no bin and .claude-plugin/plugin.json declares no executable; the plugin ships only .mcp.json, hooks/hooks.json, skills/ and dist/; src/mcp exposes seven tools and none is init; commitlore-bootstrap.sh is referenced by nothing in the tree; readme, readme-order, readme-numbers, compatibility-matrix, install-script and manifest suites pass at 116 across four languages, readme-positioning and doctor at 59, and check-readme-numbers exits 0 with the BENCH block byte-identical Unverified: whether a reader who skips the install section still meets the sentence before typing the command it explains
Say where the commitlore command comes from, since the plugin does not carry it
The hero runs promise, category, mechanism -- and then said the mechanism a second time, in the paragraph the previous positioning left behind. Three consecutive paragraphs made the same claim: "surfaces only the decisions still in force", then "tracks which decisions are still in force and which have been reversed", then "sees only current decisions when it queries a path". A reader who understood the first one had to read two more to find out they were the same sentence. In README.ko.md the leftover also broke voice. The hero above it is 존댓말 and the restatement was 해라체, so the register changed mid-hero for no reason a reader could see. Nothing is added to replace it. What the hero was missing is not another description of the mechanism but the ask, so the install block now closes with one: give the next agent the judgment the last one earned. That is the only new line in the file. The scene that shows the rejected reuse was titled "See the difference", which names a comparison rather than what the scene is about. It now states the loss. "Stop re-reviewing the same bad idea" stays underneath it, where a claim that specific is safe to make -- it is a bad hero, because it implies a verdict `guard` cannot deliver at 22% recall, but it is an accurate caption for a scene that shows exactly that one case. Record-Id: r-heropolish Limit: README.ko.md still switches from 존댓말 to 해라체 below the hero; that split is older than this change and belongs to the restructure in #344 Ruled-out: Hero nav links to install, demo and how-it-works | there is no install heading to anchor against, and minting one is a restructure, not a copy change Ruled-out: Moving the pain line back out of the demo | it was tried as the hero and moved here on purpose, and readme-order.test.ts records why Certainty: firm Blast: local Undo: easy Verified: readme, readme-order, readme-numbers, readme-positioning and compatibility-matrix pass at 83; check-readme-numbers.mjs exits 0 with the BENCH block byte-identical at 39 lines and 2262 bytes; the four positioning assertions still resolve, through the hero image alt text rather than the deleted paragraph Unverified: how the new heading reads to someone who arrives from a shared link and sees it before the hero
The commits skill opened on hand-written trailers and reached the capture pipeline only as an aside, so an agent following it end to end produced a record that nothing had checked. Capture is the surface that binds a record to a nonce, hashes the transcript and staged diff it was drafted from, and refuses a quote that does not appear in those bytes; harvest checks the quotes but binds nothing to a HEAD, a diff or an expiry; a trailer block typed from memory is checked for shape alone. The skill defaulted to the weakest of the three. Capture is the body of the file now, in the five steps an agent actually executes -- prepare, draft, verify, stage, commit -- with the MCP tool names and argument shapes alongside the CLI equivalents, and with the part no prompt contract carries: the five gates prepare-commit-msg applies at commit time, and the five-minute window that opens at stage. Every command and tool name in it was run against a scratch repository before it was written down. Hand-writing moved to a fallback that states when it applies -- a commit that already exists, or a repository without the hooks -- and points at backfill for past commits, which reconstructs through the same verified loop instead of from memory. The harvest walkthrough went with it, and the drafted-JSON example with that: prepare's prompt prints both at the moment they are needed, so a second copy here was paid for by every session that loads this file, in order to teach the older path first. Record-Id: r-captureskill Limit: a skill loads into every agent's context, so a paragraph duplicating what the runtime prompt already prints is paid for on every session Ruled-out: Documenting both paths as equals | an agent takes the first workable path it reads, and the manual one produces a record no checker has seen Ruled-out: Dropping the vocabulary table along with the harvest walkthrough | it is what the fallback writes from and what a reader parses existing records with, and neither of those has a prompt contract to lean on Certainty: firm Blast: local Undo: easy Verified: prepare/verify/stage round-tripped over the real stdio MCP server and through commitlore capture --draft in a scratch repo, both reaching a commit whose trailer block the hook appended; a fabricated quote was refused as evidence-not-found at exit 0; manifest, doctor and agent-configs suites pass at 99; the file is 151 lines against 162 Unverified: whether an agent reading only this file installs the hooks it names -- the file points at commitlore-setup rather than restating init
Record a decision, then `git reset --hard` before pushing, and the record you abandoned goes on answering. `git notes` is keyed by object name and knows nothing about refs, so the mirror outlives the commit it annotates: the object stays addressable until gc prunes it, and `git notes list` keeps naming it. `readNoteRecords` enumerated that list with no reachability filter at all. Both failure directions land at once, which is why this is a bug and not a stale extra row. The abandoned record is served as active, and its `Supersedes:` retires the record that survived in history -- the audit of v0.5.1 saw `context` answer argon2id out of a commit that no longer exists while withholding the bcrypt record `git log` still shows. `stale` answered correctly on the same repository the whole time, because `commands/stale.ts` has always intersected its notes with the commits its walk saw. The filter is that intersection, and the set is the `rev-list HEAD` the callers already made: `rebuildIndex` and `scanTrailers` hand over the walk they were going to pass to `readCommitRecords` anyway, so the fix costs no git process on either path. Only `indexNotes` pays for one, and only when the notes ref has moved -- it re-reads the mirror whole and holds no walk of its own. A non-fast-forward HEAD move is already a full rebuild, which is what keeps the incremental path from carrying a note whose commit it just orphaned. HEAD is deliberately the boundary rather than every ref. The commit source has only ever read `rev-list HEAD`, so anything wider would rebuild the same disagreement in the other direction and let a branch nobody is on retire a record that is live on the one you are. A note whose commit is already pruned was never the problem: `git gc` deletes the unreachable object and leaves the note behind, `cat-file --batch-check` answers `missing`, and the existing type filter drops it. What this closes is the window before gc runs -- two weeks to ninety days by default -- which is exactly the window in which "record a decision, then rebase before pushing" leaves a phantom behind. Record-Id: r-notereach Limit: reachability is decided against HEAD alone, so a record mirrored onto a live branch that is not checked out is not served until it is Ruled-out: Filtering against every ref instead of HEAD | the commit source reads rev-list HEAD, so a wider boundary for notes puts the two sources back out of step in the other direction and lets an abandoned branch supersede a live record Ruled-out: git merge-base --is-ancestor per annotated commit | it spends a process per record to answer what the HEAD walk the callers already hold answers for nothing Certainty: firm Blast: module Undo: easy Verified: the issue's sequence is now a test -- commit A carrying r-old001, commit B carrying r-new001 and Supersedes r-old001, B mirrored into refs/notes/commitlore, reset --hard A -- and it failed on "expected [ ...(5) ] to not include 'r-new001'" before the filter and passes after; index-db, notes, stale and query pass at 241 and the whole suite at 1939 passed and 1 skipped across 78 files; npm run typecheck is clean and dist is rebuilt from this source Unverified: how many repositories keep records on a branch other than the checked-out one, which is what the HEAD boundary narrows
Cut the fourth restatement of the hero and name the payoff once
Make the verified capture path the one the commits skill teaches
Stop serving a record from a commit the history no longer reaches
`prepareCommitMsgStub` and `postCommitStub` are the commit-msg stub with two string replacements applied, and those replacements renamed everything except the ending. So both inherited `exit 1` when no CLI can be found -- a refusal written for a validation gate, running in two hooks that never had a verdict to withhold. Nothing had been checked and rejected there; the checker was absent. That stayed invisible while `init` and the checkout agreed, and `hooks uninstall` is what pulled them apart: `init` installs three hooks and this command removed one, so the other two had no removal path at all. Reproduced on the released bundle -- three hooks after `init`, one removed, and once the pinned checkout is gone every `git commit` in that repository aborts with `commitlore: cannot find the CLI this hook was installed with`, naming a remedy that requires the binary that was just deleted. The ending is a parameter of the shared stub text now, not a third `replaceAll`. Rewriting `exit 1` to `exit 0` on the way past would have been a smaller change, and it would also rewrite any future `exit 1` added to the body above it -- which is the shape of this defect rather than a fix for it. The gate's text is untouched and `commitMsgStub()` is byte-identical, which matters because idempotence is a comparison against exactly these bytes. The other two get an ending that says what did not happen and exits 0, prescribing `init`: `hooks install` writes the gate alone, so it is not the command that puts those two files back. `hooks uninstall` now walks all three, each recognised by its own marker, and no longer stops early on a gate that is absent or foreign -- the three are installed independently and can be in any state. Its description said it removed `commit-msg`, and four READMEs named the two it left behind; both now say it removes all three. Record-Id: r-failopen Limit: with the gate installed and no CLI resolvable, commits are still refused -- that is the one hook holding a verdict back, and this change does not reach it Ruled-out: A third replaceAll turning exit 1 into exit 0 | it rewrites any future exit 1 added to the shared body too, which is how these two acquired this ending in the first place Ruled-out: Adding hooks uninstall --all and leaving the default alone | the hooks left behind are the reported defect, so a default that still leaves them documents the trap instead of removing it Ruled-out: Having the two hooks say nothing at all | a hook that captured no record and reported nothing is the silent-success shape #63 and #67 were, and the state it hides is one command away from being fixed Certainty: firm Blast: module Undo: easy Verified: against the built bundle -- `hooks uninstall` after `init` leaves no commitlore hook behind, and with the recorded checkout deleted and no commitlore on PATH both capture hooks report and exit 0 while the commit lands, where the gate installed in the same repository still refuses with exit 1; commitMsgStub() is byte-for-byte what it was; hooks, prepare-commit-msg-capture, post-commit-capture, init and doctor suites pass at 155, readme, uninstall, cli, squash and nine others at 188; typecheck clean and dist rebuilt Unverified: whether anyone already holds a repository wedged by the released stubs -- this reaches them only through `commitlore init`, which needs the CLI they no longer have
The contract at the top of this file is one sentence: exit 1 means "the record is wrong" and nothing else, because a commit-msg hook branches on it. Anything else that reaches 1 refuses a commit for a reason its author cannot fix by fixing the record. Two things did. A shallow clone does not carry the ancestor that declares the referenced id, so `Supersedes: r-old001` reports `dangling-ref` and the commit is blocked -- while the same message against a full clone of the same origin reads `references ok`. The record was never the problem; the history is truncated. `action/lint/lint.mjs` already refuses to lint a shallow checkout, because a green over fewer commits than the pull request contains is a lie. The same fact reaches the local hook from the other side, and it must equally refuse to report red. Only `dangling-ref` is withdrawn, and only when the clone is shallow. It is the one reference answer that reads straight off the ancestors. `duplicate-id` between a message's own blocks, or against a note on the same commit, is answered from the message alone -- truncation cannot reach it, and it is exactly the multi-block squash shape this command is most useful for. Withdrawing the whole class, which is the shape the `unfetched` gate at the top of the loop uses, would be a skip with no cause: a real red dropped in order to fix a false one. Reproduced through the built binary -- a two-block message sharing a `Record-Id` still exits 1 on the shallow clone. The skip is stated rather than taken. When a reference is withheld the check reports `not-checked`, naming the boundary and the fix, instead of `ok`: the status is what a machine branches on and the green is what a person carries away, and neither should read as a verdict this clone cannot give. The second was identity. `indexedHeadRecords` keyed rows on `(sha, source)` while the index's own unique key is `(commit_sha, source, block, seq)`, so every block of a multi-block commit folded into a single record and `trailerValue` read only the first `Record-Id` in it. `query.ts` keys on all four specifically to stop this. Within one message the answer is in the spec rather than the index: SPEC 2.4 closes by saying `Follows:`/`Supersedes:` resolve against `Record-Id`s regardless of which block declared them, so the comment calling a sibling carve-out future work was describing a deviation, not a boundary, and it is gone. Multi-block messages are what this project's own squash inheritance and GitHub's squash button both produce, so neither path was an edge. Record-Id: r-exitonemeans Limit: a shallow clone cannot tell a reference that resolves below the boundary from one that resolves nowhere, so neither verdict is available and the check can only name the question it could not answer Ruled-out: Skipping the whole reference check on a shallow clone, the shape the unfetched gate already uses | it would also withdraw duplicate-id, which is answered from the message alone and is the multi-block squash shape the hook is most useful for -- a real red dropped to fix a false one Ruled-out: Letting a block resolve its own Record-Id along with its siblings | a record that follows itself still resolves to nothing, and the truncation argument does not cover it, so it stays reported Ruled-out: Reporting the shallow skip as ok with the caveat attached | ok is what a machine branches on and what a reader remembers, and this clone has no verdict to offer on that reference Certainty: firm Blast: module Undo: easy Verified: three tests written red first and confirmed failing -- a shallow clone reported dangling-ref for Supersedes, a two-block message reported dangling-ref for a Follows: naming its own sibling, and a message reported dangling-ref for a Follows: naming block 1 of a commit in history; all three green after the change; validate, squash, hooks, stale and query pass at 322, and action-lint, capture-stage, cli, demo-fixture, doctor, dogfood, hooks-entry-resolution, schema, mcp-capture, gate-a-e2e, shallow-history, index-db, source-guards and help-text-honesty pass at 273; through the built binary a shallow clone now exits 0 saying not checked with the boundary named, a full clone of the same origin exits 0 saying references ok, and on that same shallow clone a duplicate Record-Id and a bad enum both still exit 1; typecheck clean and dist rebuilt Unverified: whether a reference that is genuinely dangling and also below a shallow boundary is still caught downstream -- the CI lint refuses shallow checkouts outright, so it is, but no test here walks that path
`hasShallowHistory` spawns `git rev-parse --git-path shallow`. Put first in the conjunction, that spawn happens on every `validate` -- and `validate` is what the commit-msg hook runs, so it is a process per commit in every repository that installed the gate. The overwhelmingly common case is a record with no violations at all, which has nothing for the shallow answer to decide. Testing the violation list first is free, and short-circuits before the spawn in exactly that case. Both operands are pure with respect to each other, so the verdict is unchanged; only the cost moves. Record-Id: r-shallowlast Limit: the spawn still happens once per validate that has a dangling ref, which is the case where the answer is actually needed Ruled-out: Caching the shallow answer for the process | validate is a short-lived process and a cache would outlive the fetch --unshallow that changes the answer Certainty: firm Blast: local Undo: easy Verified: typecheck clean; dist rebuilt; validate, squash and shallow-history pass at 83 with the suppression behaviour unchanged Unverified: the size of the saving on a real commit -- it was not measured, only removed from the common path
Refuse a commit only from the hook that has something to refuse
Exit 1 must mean the record is wrong, and twice it did not
`capture --help` told the caller `--diff` defaults to empty. It once did, and that was the defect: prepare hashes `git diff --cached`, an empty diff hashes to something else, and every draft came back `source-mismatch` with `no record staged`. Line 97 was rewritten to read the staged diff when the flag is omitted. The sentence describing the old behaviour stayed where it was. The cost is not untidiness. `--help` is the only place a caller learns what happens when they leave a flag off, and this one told them omitting `--diff` sends nothing -- so the correct call reads like the broken one, and they pass a file that then has to match the staged bytes exactly or be refused as a genuine mismatch. The flag exists for callers who really do have different bytes; the help text turned it into an obligation. The assertion lives beside #303's, which is the same failure in a different sentence: user-facing text that names something the product does not have. That one named a package; this one named a default. Neither had a test because neither string was read by anything. Two of the four cases are the claim and two are its teeth -- the code still has to reach for `git diff --cached` for the sentence to be worth asserting. If the default moves, both halves fail together and the wording gets re-decided instead of drifting again. Record-Id: r-diffdefault Limit: the test reads the option string out of the source rather than out of `--help` output, so a change to how commander renders descriptions would not be caught Ruled-out: Asserting on spawned `capture --help` output | the suite would rebuild the CLI for one string, and the option line is the thing under test either way Ruled-out: Leaving the string and documenting the real default only in the skill | the skill is read by agents and `--help` by people, and the two would disagree Certainty: firm Blast: local Undo: easy Verified: with the fix reverted the two claim cases fail and the two teeth cases pass; with it applied all 6 pass; typecheck clean; dist rebuilt from source and committed Unverified: whether any other option description in the CLI has drifted the same way -- only `--diff` was checked
Say the default --diff is the staged diff, which is what it became
`CaptureMode = 'suggest'` names a human approval, and the transaction it governs has nowhere to put one. The phases are prepared, verified, staged, applied, consumed -- no `approved`, no rejection, no token -- so a candidate the user kept and a candidate the user never saw are the same bytes in the same phase. `stageCaptureRecord` gates on phase, record count, HEAD, staged diff, staged tree and policy identity; none of those is a person, and none of them can be. Two hosts can behave oppositely and neither is out of contract. The prompt is therefore in the skill, between verify and stage: show what came back accepted, stage only what the user keeps. Skipping is stated as ordinary in those words, because it is -- most commits carry nothing, and an agent that reads a skip as a failure will re-ask, re-word, and argue a record back into a commit the user already declined. Silence on a trivial commit is the correct output. What that buys is this host asking, and nothing more. The three places `suggest` is described now say so: a host that stages without asking violates no check here. Overstating a host-side convention as enforcement is the same defect reported here, committed a second time, and it would leave the next reader of `CaptureMode` believing a check exists. The phase that would make it true is not built. It is a sixth member of a field ADR-0021 declares normative, in a file whose `version` consumers must reject when they do not understand it, plus a who-and-when field, which is a version bump by ADR-0021's own rule; and an approval-carrying mode is a different `mode` value, so the identity hash pinned in `test/capture-policy.test.ts` changes and every pending record in flight reads as a policy change to the hook. That is a protocol change across the CLI, the MCP tools, both hooks and every integration built on them. ADR-0028 records it as unbuilt rather than rejected, with its cost priced. Record-Id: r-asksbeforestage Limit: stage accepts a verified nonce and nothing else, by ADR-0021's design against a replayed verification -- consent cannot be an argument to it, so the prompt can only live in the caller Limit: the CLI's one-command form composes prepare, verify and stage in one process, so it still stages without asking; the skill says so and sends the asking path to the MCP tools Ruled-out: Adding the approved phase in this change | it moves the pending format, ADR-0021's normative field set and the identity-hash inputs -- the three things fixed precisely because breaking them forces a coordinated update across every agent integration, and a protocol change decided as a side effect of closing a documentation defect gets the least review Ruled-out: Documenting the gap and not shipping the prompt | a gap nothing fills is a smaller product, the UX has to live in the skill under either design, and building it now means a later phase would enforce a flow that already exists rather than inventing one Ruled-out: Removing mode from the policy until it can be enforced | it is an input to the identity hash that ADR-0021 section 7 fixed and capture-policy.test.ts pins, so deleting it changes the digest for every pending record in flight -- the full cost of the real fix for none of its benefit Ruled-out: Renaming it to something weaker like advisory | same hash cost as any other mode change, and it buys a word rather than a behaviour; the honest fix for a word that overclaims is a sentence next to it saying what it does not do Certainty: firm Blast: module Undo: easy Verified: no source behaviour changed -- the only .ts edit is a doc comment, and dist/commitlore.mjs is byte-identical after rebuild; capture, capture-policy, capture-prepare, capture-stage, capture-verify, capture-guard-advisory, capture-rejection-reasons, mcp-capture, post-commit-capture, prepare-commit-msg-capture, pending, pending-gc and pending-command pass at 169; manifest, schema, doctor, harvest, validate, no-compiled-executable, index-db, source-guards and gate-a-e2e at 294; readme, readme-numbers, readme-order, readme-positioning, compatibility-matrix, help-text-honesty, dogfood and cli at 118; typecheck clean and dist rebuilt Verified: the reported claim, read off the code -- CaptureMode has one member, KNOWN_PHASES in pending-gc.ts lists exactly the five, and stageCaptureRecord has no approval precondition Unverified: whether any host other than this skill calls commitlore_stage_capture today -- the ADR's trigger for revisiting assumes one eventually will, and none was looked for Unverified: that a verified-but-skipped transaction is harmless in practice -- it is inert by the five-gate check and cannot reach a commit, but capture gc keeps it (null expires_at, fail-closed) and pending has no rm, so it accumulates
ADR-0020 through ADR-0026 all read `Status: Accepted (YYYY-MM-DD)`. ADR-0027 and ADR-0028 read `Status: accepted` and stop, so the two most recent decisions in a repository whose entire subject is decision lifecycle are the two that do not say when they were made. The date is not decoration on an ADR. Reading a decision record later, "is this still current" is the question, and the answer starts from when it was taken. Git knows -- `git log -1 --format=%cs` on the file answers it -- but that is a lookup a reader does not know to make from a document that looks complete. Both were recorded today, which is what the file dates confirm. The capitalisation matches the seven that came before rather than the two that came after. Record-Id: r-adrdated Limit: nothing enforces the header shape, so the next ADR can drop the date again Ruled-out: A test asserting the ADR header format | it would be the first test over docs/adr/ and pins a convention that has drifted twice already; worth doing when the shape is settled, not while settling it Ruled-out: Backfilling dates from git for every ADR | the earlier ones already carry theirs, and rewriting them from commit dates would overwrite a stated fact with an inferred one Certainty: firm Blast: local Undo: easy Verified: git log -1 --format=%cs on ADR-0027 reports 2026-08-01, matching the date written; both files render with the same status line shape as ADR-0020 through ADR-0026 Unverified: whether any external link or tool parses the status line -- nothing in this repository reads docs/adr programmatically
Ask before staging, and stop calling that an enforced policy
Two tests in `test/demo.test.ts` decided whether `runDemo` had left its temporary directory behind by listing the process-wide `tmpdir()` and filtering for `commitlore-demo*`. That namespace belongs to every process on the machine, so under `npx vitest run` the answer came back from whichever worker happened to be holding its own demo directory at that moment. Both went red in two separate worktrees, naming a different leftover directory each time, and both passed in isolation -- the shape that teaches a reader to discount red, which is the one signal the commit gate depends on. The first took a before/after delta, which narrows the window without closing it: a directory another worker creates after the `before` snapshot and still holds at the `after` snapshot is absent from `before` and present in `after`, so it reads as leftover. The second took no delta at all and failed if any `commitlore-demo*` directory existed anywhere in `tmpdir()`, whoever made it. The property is worth keeping exactly as stated -- the demo must remove its temporary directory, including when it crashes mid-run -- so the fix gives the assertion a namespace it owns rather than softening it. `runDemo` now takes a `tmpRoot` defaulting to `tmpdir()`, and the suite creates one root per case and asserts that root is *empty*: no prefix filter, so nothing anyone else wrote can either satisfy or break it, which is a stronger claim than the one it replaces. Routing every demo in the file through that root also stops the suite writing `commitlore-demo-*` into the shared tmpdir, where a run in another checkout would have read it -- this suite was itself a plausible source of the failures it suffered. An option rather than an env override, because the production default has to be unmistakable: `runDemo()` with no argument is still `tmpdir()`, decided at the one line that creates the directory, where an ambient `COMMITLORE_DEMO_TMPDIR` could redirect a real `commitlore demo` run with nothing at the call site saying so. Record-Id: r-owntmproot Limit: the demo still defaults to the shared tmpdir, so concurrent `commitlore demo` runs still create sibling directories there -- that is deliberate, and it is safe only because nothing now asserts over that namespace Ruled-out: An env override such as COMMITLORE_DEMO_TMPDIR | it moves the production default off the call site, where an ambient variable can redirect a real run and nothing in the code reads as changed Ruled-out: Keeping the before/after delta and widening it | the delta narrows the window rather than closing it, and the directory that turned this red was created inside the window it leaves open Ruled-out: Deleting the two tests or dropping the prefix filter to make them pass | the property is real and cheap to hold, so that trades a flaky true signal for a permanent blind spot over cleanup after a crash Certainty: firm Blast: local Undo: easy Verified: reproduced deterministically before fixing -- a helper creating a `commitlore-demo-*` directory in the shared tmpdir every 25ms during the run, which is what a second worker does, gave `2 failed | 5 passed` with both cleanup tests printing the foreign directories; the same helper against this change gives 7 passed with 397 foreign directories live in tmpdir at exit; demo, cli and init suites 40 passed, full suite 78 files, 1942 passed with 1 skipped; the built bundle's `commitlore demo` still creates and removes under `tmpdir()` with no argument, counted 0 before and 0 after; typecheck clean and dist rebuilt Unverified: whether the two failures reported in the wild had any cause other than a concurrently held demo directory -- only that shape was reproduced
Ask the cleanup question of a directory the demo test owns
…sions At 440 lines it carried the complete record example, every protocol key, the harvest walkthrough, the whole experiment table, latency and break-even detail, and source-install specifics. All of it true, all of it checkable, and none of it what somebody needs in their first ten seconds. Reference material is not wrong for being long; it is wrong for standing between a reader and the install command. So it moves, and the README links to it. `docs/protocol.md` takes the small record example and how to read trailers with plain Git. `docs/capture.md` takes the agent prompt, the harvest and capture routes, and the by-hand escape hatch. `docs/evidence.md` takes the field report, latency and scaling, hook overhead, and the break-even that was never measured -- split into what is measured and what is not, so the boundary is read rather than reconstructed. `docs/install.md` takes the source checkout and what uninstall leaves behind. `docs/cli.md` takes the command surface, including the hook path a reader previously had to reproduce from a collapsed block. Nothing was deleted; every sentence the README carried is at the other end of a link. Three pieces stayed, because three checks bind them to the README by position and each encodes a guarantee worth more than the lines it costs. Loosening any of them to make the move fit would trade the reason the document is trusted for a shorter page. The English file is 380 lines, the translations 365 to 371, each restructured in its own voice rather than translated back from the English. Record-Id: r-readmesplit344 Limit: three checks bind content to a position in the README, so the complete record example, the protocol vocabulary table and the generated benchmark block could not move Ruled-out: Moving the complete record example to docs/protocol.md as the issue asks | spec/verify.sh compares the last fenced text block in every README byte for byte with spec/fixtures/valid/11-readme-example.txt and has no second location to read Ruled-out: Moving the vocabulary table to spec/SPEC.md | spec/schema/readme-vocab-check.mjs requires every SPEC section 3 key to appear in a table row of each README, so removing the table reports all of them as missing Ruled-out: Moving the benchmark block to docs/evidence.md | scripts/check-readme-numbers.mjs regenerates and byte-compares it in README.md and test/readme-numbers.test.ts asserts the marker in all four, so a moved block is an absent block Ruled-out: Loosening any of those three so the issue's table could be followed literally | they are the honesty guarantees the document is trusted for, and a restructure that weakens them buys length with credibility Ruled-out: Adding docs/COMPATIBILITY.md to the new documentation index | test/compatibility-matrix.test.ts pins each README to exactly one pointer, and a second link would have failed it Certainty: firm Blast: local Undo: easy Verified: readme, readme-order, readme-numbers, readme-positioning, compatibility-matrix and manifest pass at 107; demo-recording, help-text-honesty, agent-configs and no-compiled-executable pass at 37 with the new docs staged; scripts/check-readme-numbers.mjs exits 0 reporting the block byte-identical at 2262 bytes; spec/verify.sh reports 25 fixtures plus README example sync plus vocab table; every relative link in the four READMEs and the five new docs resolves on disk Unverified: whether a reader who follows the links finds what they need faster -- the reduction is measured in lines, not in comprehension
`validate` runs two independent detectors, and one finding sits under both. A `Record-Id` repeated across a message's own blocks is caught by the shape check, which reads it off `labelRecordBlocks` from the message alone, and again by `checkReferences`, where those same blocks enter `findIdCollisions` as sibling records. The two lists are concatenated with nothing between them. A two-problem message therefore prints four lines, and the summary says four. That number is what a person reads at the moment their commit was just rejected, and it is how they judge how much work they are in for. `--json` carries the same objects byte-identical in pairs, so the repair loop is handed two identical instructions for one edit. Pre-existing, not a regression from #352: the same input against builds at c9a83a2 and at ca538a6 produces the identical four lines. It survived because the reference half is invisible until the `unfetched` gate is past -- a repository with no notes refspec short-circuits `references` to `not-checked` and prints two, which is the common local case and reads correct. Deduped at the seam between the two lists rather than by deciding which check owns the rule, because neither can be the one that goes silent. The shape half is the only half that survives the `unfetched` and shallow gates and the stdin mode that identifies no repository at all -- the commit-msg hook's own path. The reference half is the only half that answers on a resolved `sha`, where the shape half deliberately stands down, and the only half that can see a collision against a note or against an earlier commit at all. The overlap between them is real; only the second copy is the defect. The key is every field that reaches the output -- sha, line, rule, key, value, got, want -- so the multiplicity each check produces on its own is untouched: still one `duplicate-id` per colliding block, still one `cardinality` per extra occurrence. Neither check's status is softened. `shape failed · references failed` stays true, because both of them did fail; what changes is that they no longer say the same thing twice. Record-Id: r-dedupviol Limit: the key includes `line`, so two detectors that locate one finding differently -- one with a line, one without -- would still print it twice; today both resolve the line through the same `locateTrailerLines`/`lineForViolation` path Ruled-out: Letting checkReferences stay silent on duplicate-id | it is the only reporter once an sha is resolved, so --commit and --range would lose the finding outright, and the bug-issue-92 case in validate.test.ts asserts `references failed` on exactly that input Ruled-out: Letting the shape check stay silent on duplicate-id | it is the only half that runs when the notes mirror is unfetched, the clone is shallow, or the message arrives on stdin, so the check would disappear precisely where a commit is being written Ruled-out: Deduping the merged list globally instead of across the seam | it would also fold two byte-identical unknown-key trailers into one, and those are two edits rather than one instruction printed twice Certainty: firm Blast: local Undo: easy Verified: the issue's reproduction, in a clone with the notes refspec configured, printed 4 violations and 4 --json objects before and prints 2 and 2 after, exit 1 either way; the new red case in validate.test.ts fails with [4, 8, 4, 8] against the old code; validate, dogfood, squash, hooks, schema and cli pass at 220, and stale, trailers, query, notes, source-guards, gate-a-e2e, index-db and doctor at 457; typecheck clean and dist rebuilt from source Unverified: whether any rule other than duplicate-id is found by both check classes on one input -- only this one was traced, and the key is content-based rather than rule-specific, so a second overlap would fold without anyone noticing
Report a duplicate-id once, not once per check that found it
Move the README's reference manual into docs/ and link to it
…at it `chronological` orders on `committedAt`, which carries `%ct` -- one second of resolution -- and breaks a tie on input array position. Both paths that serve records to an agent fed it newest-first: `commands/stale.ts` handed over the `git log` walk as it came, and `core/query.ts` handed over index rows ordered `committed_ts DESC, commit_sha ASC`. So "the latest declaration wins" resolved to the *oldest* of two same-second commits on the stale path, and on the query path to whichever commit sha happened to sort last. Reproduced on two repositories differing in one blob. `context` answered from the newer declaration in the first and from the older in the second, purely because the shas sorted the other way; `stale` answered from the older in both. One record in one repository, and what it said depended on which command was asked and on a hash. `commands/validate.ts` has reversed the walk since bug-issue-187, with a comment saying why. `buildReport` now does the same, and `foldStates` sorts the index rows with the comparator `mergeByIdentity` was already using, so a query's lifecycle and its payload can no longer come from opposite ends of a tie. Determinism was the cheap half. Reversing the git walk recovers a real topological order -- a walk never emits a parent before its child -- but the index has nothing equivalent to recover. `trailers.id` looks like an ordinal and is not one: `rebuildIndex` inserts in `git rev-list HEAD` order, newest first, and `updateIndex` appends later batches above it, so it runs backwards inside a rebuild and forwards across them. A real ordinal would be a schema change, which is an ADR's decision rather than this fix's. So where the order cannot be recovered, the engine stops answering. Two commits in one second that declare one `Record-Id` with different values for a non-repeatable key are routed to `hasAmbiguousGroup`, the treatment a divergent note and a twice-declared block already get: `duplicate-id` from `stale` and `validate`, and content withheld under `[blocked]` on every consumer route. When the disagreement is `Expires:` the fold also declines to retire the record and flags it for review, because the two candidate dates were "live all year" and "lapsed in January" and picking one is how an agent is handed a constraint that expired months ago. Declarations that agree are left alone. A rebase collapsing committer dates across a batch is ordinary, and a same-second follow-up that only adds a repeatable key folds to the same record in either order -- there is nothing there to refuse. Record-Id: r-secondtie Limit: committed_ts is %ct at one-second resolution and the index stores no ordinal that orders two commits inside one second, so a tie on that path can be made deterministic but never topological Ruled-out: Adding a topological ordinal to the trailers table | it bumps SCHEMA_VERSION and changes the index format, which needs an ADR and the representative's approval rather than a silent column Ruled-out: Reusing trailers.id as that ordinal | rebuildIndex inserts in git rev-list HEAD order, newest first, and updateIndex appends newer batches after it, so the rowid runs backwards within a rebuild and forwards across them Ruled-out: Sorting both serving paths by (committed_ts, commit_sha) so they agree exactly | it buys symmetry by discarding the one real topological signal available, the order of the git log walk, making the path that has a signal as arbitrary as the path that has none Ruled-out: Resolving the conflict deterministically and saying nothing | determinism only makes two commands agree on an answer neither of them earned, and this repository treats a confident wrong answer as worse than a declared unknown Ruled-out: Adding a fourth Lifecycle value for the undecidable case | active plus the existing review flag already says a human must decide, and a new enum member is a resolution-contract change every consumer would have to be taught Certainty: firm Blast: module Undo: easy Verified: the issue's fixture reproduced the split -- context returned no active records while stale reported 1 record and 0 stale -- and a second fixture differing only in one blob flipped context to the older declaration, which is the sha tie-break made visible; six new tests were run red against dev first; after the fix both commands report the record under review with an id collision on both fixtures, on the index and --no-index paths; the full suite passes at 1955 across 78 files; typecheck clean and dist rebuilt Unverified: whether an index ordinal could be added cheaply -- the schema change was not attempted, only ruled out for this fix; RecordState.sha and the order of repeatable trailers still follow the arbitrary tie-break when two same-second declarations agree, which changes attribution but not content; the structural Certainty: that survives withholding on a blocked record still comes from whichever declaration the index tie picked
Refuse the tie two commits in one second create, instead of guessing at it
…ne now `expires_at` is stamped at stage time, so a transaction that stops at `prepared` or `verified` carries `expires_at: null` for the rest of its life. `pending-gc.ts` requires a parseable expiry before it will consider a file and keeps it otherwise, which reads as fail-closed and is not. Fail-closed protects a file that might still be finalised; on these two phases that branch could never fire at all, so the file was kept for ever. `commitlore pending` had `ls` and `show` and no `rm`, so nothing could remove it either -- `pending ls` could only name the condition, which is what its `never-collected` flag was saying. because it is -- most commits carry nothing worth recording -- and every skip is a `verified` transaction deliberately never staged. The intended common outcome leaked a file each time, and `pending ls`, which exists to show what is genuinely in flight, filled with what is not. `prepared` and `verified` now age out on `created_at`, the only timestamp they have. Age alone is not permission: the file is collected only once HEAD has moved past `base_head`, which is exactly what `stageCaptureRecord` refuses on, so a transaction that could still be staged is left alone however old it is. `pending ls` already derived that as its `stale` flag; both callers now ask one function, because two copies of "can this still be finalised" is how a listing and a collector come to disagree about the same file. The window is 24 hours, chosen rather than inherited. Staleness is what makes collection safe; the window is margin for the one way that proof goes temporarily wrong, since an amend, a reset or a branch round-trip can put HEAD back on `base_head`, and those cycles run in seconds rather than days. It is the same 24 hours as the consumed window because nothing here argues for two retention stories in one directory. Every other branch is untouched: `staged` and `applied` are still protected outright, `consumed` still ages on its own clock, and a file whose phase or age cannot be read is still skipped. `pending rm <nonce>` covers the user who wants the file gone before then. It refuses `staged` and `applied` and names the phase in the refusal, because the post-commit hook may still finalise either into a record -- the same reason gc protects them -- and it refuses a file it cannot read for that reason inverted: an unknown phase might be one of those two. Record-Id: r-gcunstageable Limit: ADR-0021 fixes the pending format and stamps `expires_at` at stage only, so giving these phases an expiry earlier is a format change rather than a fix Limit: gc runs only when `capture gc` is invoked -- nothing schedules it, so a leaked file goes at the next run rather than at the 24-hour mark Limit: staleness is derived from `base_head` against HEAD; a transaction whose staged diff moved while HEAD did not is equally unstageable and is still kept, which is the conservative half of the same test Ruled-out: Collecting on age alone, without the staleness test | a transaction still sitting on the HEAD it was prepared for can be staged and committed at any time, so age alone would delete work the user is still holding Ruled-out: Stamping expires_at at prepare or verify | it moves a field ADR-0021 declares normative and whose null-before-stage shape an mcp-capture oracle pins, and it would expire a capture while the user is still being asked about it Ruled-out: A second staleness rule inside pending-gc.ts | `pending ls` already computes the same question, and a listing that calls a file collectable while the collector disagrees is worse than either answer alone Ruled-out: Deleting the transaction where the skill skips it | the skip lives in a skill a host is free not to run, and a leak avoided only on the well-behaved path is not fixed Ruled-out: A --force for `pending rm` on a staged or unreadable file | an unreadable file may be a newer format this binary cannot parse and another still can, and the two phases it protects are the ones a hook may be seconds from finalising Limit: a `staged` transaction that is never applied is still kept for ever -- the hook skips it once `expires_at` passes and gc protects the phase -- which is a separate leak this change deliberately does not touch Certainty: firm Blast: module Undo: easy Verified: RED first -- the two collection assertions in test/pending-gc.test.ts failed while the four protective oracles beside them passed; after the change pending-gc at 23, pending-command at 14, and the capture, cli, mcp-capture, hook-capture, help-text-honesty, source-guards and manifest suites with them at 246 over 18 files; the whole suite at 78 files, 1962 passed and 1 skipped Verified: the issue's reproduction re-run against the rebuilt bundle -- `capture gc` keeps the prepared file while HEAD is unchanged, keeps it once stale but inside the window, and removes it with `created_at` back-dated 25 hours; `pending rm` deletes a prepared one and refuses a staged one by naming the phase Verified: npm run typecheck clean and dist/ rebuilt by npm run build Unverified: whether 24 hours matches how long a prepared transaction actually sits in practice -- the window is argued from amend-cycle duration and the existing consumed window, not measured against real usage Unverified: the HEAD round-trip the window exists for is reasoned about rather than exercised -- no test moves HEAD off `base_head` and back before gc runs
Collect the capture that can never be staged, and let a user remove one now
Issue #343 asks how much of a repository's active decision set a fresh agent recovers before its first edit, and says why the question is answerable: the repository holds the records, so recall against them is countable, and a record that was retired and still surfaced is countable as an error. A metric written once the numbers are on screen is not a measurement, so this commit carries the method and nothing else -- no harness, no result, no figure. The next commit implements it and the one after that runs it. If a definition here turns out to be inconvenient when the numbers exist, the history says which came first. Two decisions are the ones worth arguing with. The primary denominator is the active records attached to the path being edited, not every active record in the repository: the question is what an agent has before *this* edit, and a repository-wide denominator would make every route's score a function of how large the repository has grown rather than of what bears on the change. The repository-wide figure is reported next to it anyway, so the choice can be checked instead of taken on trust. The second is that this is not ADR-0017's registered study, and must not be read as it. That protocol scores an agent's written brief against gold annotated from ordinary project evidence, and it rules out CommitLore records as gold for exactly the right reason: a brief scored against them would measure whether the tool can read its own encoding. This measures bytes delivered, which is a ceiling on what any agent could recover -- and for a ceiling the record set is the only correct denominator. The registered pilot stays unrun. Record-Id: r-delivmethod Limit: what a route hands over bounds recovery from above and is not recovery; nothing measured under this method can say an agent read or used a delivered record Ruled-out: Running ADR-0017's registered pilot instead | it needs two independent annotators and forty agent runs, while #343 asks a narrower question that this repository can already decide Ruled-out: Every active record in the repository as the primary denominator | it scores a path-scoped route against a quantity that grows with the repository rather than with the edit, so it is reported beside the primary rather than as it Ruled-out: Writing the method after the first run | a definition chosen once the numbers are visible cannot be told apart from one fitted to them Certainty: firm Blast: module Undo: easy Verified: this commit adds one document and no code; nothing is measured by it Unverified: whether this corpus exercises the expiry half of the error term, which only the run can report and this registration cannot promise
The measurement registered in bench/DECISION-DELIVERY.md needed a harness, and the shape it had to fit was already in the repository. The deterministic suite writes rows carrying harness_commit, harness_digest and dist_digest, refuses a dirty checkout, refuses mixed provenance, re-checks that neither HEAD nor dist moved during the run, and renders its own markdown beside the JSONL. So decision_delivery is a metric of that suite rather than a script beside it, and COMMITLORE_DETERMINISTIC_RECOVERY_ONLY=1 follows the DENSITY_ONLY escape hatch that already exists for running one metric against this repository's own history. It is also a required metric, so a complete suite run cannot quietly omit it. The answer key lives in its own module for one reason: it must not be able to reach the product. bench/deterministic/census.ts walks git log, parses trailers with Git's own interpret-trailers, reads changed paths from git show, and re-implements the SPEC 5 lifecycle fold; a test fails the build if an import from src/ or dist/ ever appears in it. Scoring a projection against a key that projection computed would be worth nothing. Where the two folds could disagree the row says so rather than hiding it: it carries the Supersedes: trailers the block walk resolved beside the count a raw line scan of the same messages found, and the number of records sitting on commits that changed no path. The arms reuse the ablation flags inject.ts already exposes -- noScope and noLifecycle together are the every-record arm ADR-0017 registered -- rather than a second projection written for the bench, which would measure the bench's idea of an unfiltered dump instead of the product's. One consequence outside the metric: a single-metric report no longer appends the economic case. That section cites another run's file, and a one-metric dataset carrying it reads as though it measured costs it never touched. Record-Id: r-delivharness Limit: gold attaches a record to the paths its commit changed and follows renames, which is also how the shipped projection decides scope, so a high commitlore recall is partly definitional and only the budget, grading and index resolution can move it below one Ruled-out: A standalone script beside bench/, in the shape of bench/retrieval/compare.ts | it would carry a source hash instead of the harness_commit and harness_digest ADR-0018 requires, and nothing would stop a full suite from omitting the metric Ruled-out: Deriving the answer key from runQuery or the record index | that scores the product's retrieval against the product's own census, which ADR-0017 rules out for the same reason Ruled-out: Resolving the rename chain only for paths with no direct attachment | the saving is one git call per path and the cost is dropping exactly the pre-rename records the measurement exists to count Ruled-out: Exempting the delivery run from the concurrent-bench refusal, as density is | r-densitymerge135 exempted seconds of git log, while this run spends minutes projecting every path and is exactly what would corrupt a wall-clock arm's percentiles Certainty: firm Blast: module Undo: easy Verified: test/decision-delivery.test.ts passes at 18, including the import ban that fails the build if census.ts ever reaches into src/ or dist/, and a fixture whose superseded record ordinary git log surfaces and the shipped route does not; deterministic-bench, density-denominator, economics and bench-provenance pass beside it at 66 total; src and bench typecheck clean and npm run build leaves dist byte-identical Unverified: whether the two lifecycle folds agree on this repository's corpus, which the run reports and this commit cannot
…te does The run is in. Of the 2,149 active (path, record) pairs this repository holds for files an agent could edit, the shipped path-scoped projection delivers 1,764 -- 82.1% -- and not one of the 1,900 records it hands over has been retired. That is the answer to #343, and it is not the best recall in the table. Ordinary `git log -- <path>` delivers 94.2%, twelve points more, and leaves no path empty-handed where the scoped route delivers nothing on 117 of 1,037. It pays for that with 75 retired records and 2.6 times the tokens. Recall is the side of that trade the product loses, and the comparator is reported first because a table showing only 82.1% would read as a win. The unfiltered dump is not a third option. At the shipped budget it recovers 2.3%: eight hundred tokens buys ten records for the entire repository and they are almost never the path's. Unbudgeted it reaches 92.1% for 83 million tokens and 7,259 retired records surfaced. The rows locate most of the missing 17.9 points without another run. The unbudgeted dump misses exactly 170 pairs, which is exactly the shipped route's withheld_records count: ten records in this repository grade `blocked`, so 92.1% is the ceiling for any injection route here and the trust grader sets it, not path scoping. The remaining ten points are the difference between a path-scoped projection and a repository-wide one, and no committed row splits them further, so the write-up does not guess. The last finding is about the question rather than the tool. Two independent implementations of "which records belong to this file" disagree on 215 pairs one way and 136 the other. Any recall figure inherits whichever definition its author picked, and this one says which. Record-Id: r-deliv821 Limit: one corpus, one repository, one query strategy per route; delivery bounds recovery from above and no agent was run Warn: zero retired records delivered is evidence about the supersede filter on seven records and no evidence at all about expiry, because this corpus contains none Ruled-out: Publishing only the path-scoped route's figure | the comparator is what makes 82.1% mean anything, and the route that beats it is the one a reader would otherwise assume was worse Ruled-out: Splitting the remaining ten points into budget and path scope | no committed row separates them, and a number derived from a second uncommitted script is exactly what this project refuses to publish Ruled-out: Rerunning with the report tightened first | the artifact and the renderer that produced it stay in step, which is worth more than removing one repeated sentence Certainty: firm Blast: local Undo: easy Verified: the run is bench/results/decision-delivery-20260801T051755Z.jsonl and its generated .md, ten rows sharing one harness commit, one dist digest and one machine descriptor; every figure in section 9 was read back out of that jsonl rather than transcribed; node scripts/check-readme-numbers.mjs reports the README block byte-identical at 39 lines and 2262 bytes; src and bench typecheck clean, dist rebuilds identical, and decision-delivery, deterministic-bench, readme-numbers and dogfood pass at 62 Unverified: how many of the ten blocked records are false positives, which this run counts but cannot adjudicate; and whether any agent would use what it was handed
The first run's headline read "ordinary git log recovers twelve points more than the shipped route". That comparison put an unbounded `git log` against an 800-token projection, so it moved two things at once and licensed a statement about neither. Section 9.3 of that run already said the residual could be the budget; the table was still published as though it were the mechanism. Understating the product on a measurement that does not support the claim is the same class of error as overstating it. Two arms fix it, and they are registered here before the run that reports them, for the same reason the metric was: an arm chosen after seeing what it does to the number is not a measurement. `commitlore-unbudgeted` is the path-scoped projection with the cap removed. Whatever it still misses is scoping or grading; the rest of the shortfall is the cap. `git-log-path-budgeted` is `git log` for the path cut to the same 800 tokens -- the comparison a real agent faces, because context is finite and an unbounded route is not one anybody can use. Which end the Git arm is cut from decides its number, so the section says. `git log` prints newest first and `buildInjection` drops its tail, so keeping a prefix cuts the same end the product cuts. The trailing partial line goes with it: a Record-Id line severed halfway was not delivered, and an anchored line scan would otherwise match it against the end of the string. Both original arms stay and are re-measured, so the two runs are readable against each other. The first run keeps its own section rather than being deleted; it measured the five arms it covered. Record-Id: r-delivpaired Limit: an arm added after a run cannot be presented as pre-registered, so it is dated and reasoned in the section it changes rather than folded into the original text Ruled-out: Leaving the confounded table and explaining it in prose | the explanation sat under a table whose top line already said the opposite, and a reader who stops at the table has been told the wrong thing Ruled-out: Dropping the unbudgeted arms and reporting only budgeted ones | the ceiling a mechanism reaches with unlimited context is the other half of the attribution, and one row cannot carry both Ruled-out: Cutting the budgeted git arm from the oldest end | git log prints newest first and the projection drops its tail, so cutting the head would score a route the product does not implement Certainty: firm Blast: module Undo: easy Verified: this commit changes one document and no code; the harness that implements these arms lands next, and the run after that Unverified: whether the shortfall is the cap or the scoping, which is the question the arms exist to answer and which no run has yet answered
Implements the arms registered in bench/DECISION-DELIVERY.md section 5. Nothing about the metric, the denominator, the error term or the scoring moves; the route list grows from five entries to seven and the budget column stops being decoration. The two git arms read one `git log`. Running the command twice would double the run for two views of the same bytes, and more importantly the budgeted arm has to be the same text its unbudgeted twin saw, or the pair is no longer a controlled comparison of one variable. `truncateToBudget` keeps a prefix, which cuts the oldest end because `git log` prints newest first and `buildInjection` drops its tail. It also discards the line the cut severed. Without that the anchored `Record-Id:` scan matches a half-line against the end of the string and scores a record that was never delivered -- a test pins both halves of that. The truncation assertion that guarded the unbudgeted every-record arm now guards both unbudgeted arms, because a silent cut on either would read as a property of the route rather than of a constant this file chose. Record-Id: r-delivarms Limit: the paired rows separate the cap from the mechanism only while both members read identical inputs, which is why the git pair shares one command's output rather than issuing two Ruled-out: A separate git invocation per arm | two invocations can disagree under a concurrent write, and the pair would then differ by more than the budget it exists to isolate Ruled-out: Counting a Record-Id line the budget cut in half | the anchored scan matches it against the end of the string, which would credit a record the route never handed over Ruled-out: Widening the truncation assertion into a warning | an unbudgeted arm that truncated is a wrong number, and a warning in a log nobody reads is how it would ship Certainty: firm Blast: module Undo: easy Verified: test/decision-delivery.test.ts passes at 21, including a truncation case that pins which end is cut and that the severed line is not scored, and a monotonicity check that no budgeted arm reaches past its unbudgeted twin in recovered records or tokens; deterministic-bench, density-denominator, economics, bench-provenance and dogfood pass beside it at 57; bench and src typecheck clean and dist rebuilds identical Unverified: what the added arms report on this repository, which the next commit measures
…ehind The paired arms are in, and they reverse the first run's conclusion. Against `git log` for the same path cut to the same 800 tokens, the shipped projection delivers 81.7% of this repository's 2,217 active (path, record) pairs where ordinary Git delivers 42.0%. It leaves 117 paths empty where Git leaves 183, it surfaces 0 retired records where Git surfaces 7, and it spends fewer tokens doing it -- 511,412 against 673,134 -- because raw commit prose carries subject lines and bodies the projection does not render. The first run reported the opposite because it compared an unbounded `git log` against a capped projection. That number is real and it is still in section 10; it is a statement about the cap, and it was published as though it were a statement about path scoping. Section 10 now says so at the top, because a reader who stops at its table would otherwise carry away the wrong conclusion. The two new arms also settle what the first run could only bound. Removing the cap takes the projection from 1,811 pairs to 2,047, so the cap costs 10.6 points. The repository-wide dump with its cap removed recovers 2,047 pairs as well -- the same number -- so path scoping costs nothing at all, and it reaches that with 741,429 tokens against 92,175,612 and 0 retired records against 7,322. Whatever holds the shipped route below 100%, the path filter is not it. What does hold it there is the trust grader. The unbudgeted projection misses exactly 170 pairs, which is exactly the withheld_records count on both CommitLore rows: ten records here grade `blocked`, so 92.3% is the ceiling for any injection route on this corpus. Unbudgeted against unbudgeted, ordinary Git is 46 pairs ahead precisely because it renders those, and pays 75 retired records for the privilege. docs/evidence.md exists on dev now, so the summary goes there beside the other measured results, with the method and the full tables staying in bench/ next to the run that produced them. Record-Id: r-delivpairedresult Limit: one corpus, one repository, one query strategy per route; this is delivery, which bounds recovery from above, and no agent was run Warn: the corpus holds zero expired records, so 0 retired records delivered is evidence about the supersede filter on seven records and none at all about expiry Ruled-out: Deleting the first run once it was superseded | it is a correct measurement of the five arms it covered, and a project that removes the run that embarrassed it cannot be checked Ruled-out: Correcting the first run's numbers in place | its rows were produced by a harness that had no budgeted git arm, and editing a conclusion under unchanged rows is how a result stops matching its evidence Ruled-out: Leading with the unbudgeted comparison, where git log is still ahead | no agent has unbounded context, so that row is a mechanism's ceiling rather than a route anyone can take Certainty: firm Blast: local Undo: easy Verified: the run is bench/results/decision-delivery-20260801T060225Z.jsonl and its generated .md, 14 rows sharing one harness commit, one dist digest and one machine descriptor; every figure in section 9 and in docs/evidence.md was read back out of that jsonl rather than transcribed; node scripts/check-readme-numbers.mjs reports the README block byte-identical at 39 lines and 2262 bytes; decision-delivery, deterministic-bench, readme, readme-numbers, readme-order, readme-positioning, help-text-honesty, economics and dogfood all pass; src and bench typecheck clean and dist rebuilds identical Unverified: how many of the ten blocked records are false positives, which this run counts but cannot adjudicate; and whether any agent uses what it was handed
Measure how much of the active decision set reaches a fresh agent: 81.7% at the shipped budget
…rest `Ruled-out:` separates on the first `|` and has no escape, so an alternative that quotes a shell pipeline is truncated mid-sentence. The record then cannot match the thing it rules out: against a proposal quoting the real alternative, `guard --threshold 0` returned no match at all, while the truncated fragment matched at 0.75. Nothing said so. `validate` exited 0 with `shape ok`, and `commitlore ruled-out` printed the value verbatim -- which reads exactly like a value whose separator landed where its author meant it to. Which end to split from is a question about real records, so it was counted rather than argued. This history holds 620 distinct `Ruled-out:` values. Three carry more than one pipe. Two of those three carry it in the *reason* -- `||` quoted from shell prose in aa68a9a, `.mjs|.js` quoted from a filename alternation in 616005d -- and both parse correctly today. The third, 7bf6ced, carries it in the alternative (`irm | iex`) and is a live instance of this bug in our own history. Splitting on the last pipe would destroy two correct records to rescue one broken one, so the first pipe stays the separator. That leaves the silent half, and it is closed in two pieces because the evidence does not support one. An alternative whose code span the separator closes outside of is not a judgement about intent. The span opened before the pipe and closed after it, so the pipe was inside quoted text and the alternative is a fragment ending mid-span. That is a `format` violation now, refused at commit time by the schema. It rejects none of the 620 -- backticks are balanced in every one -- and none of the fixtures. A value that merely carries a second pipe cannot be refused without taking the two correct records with it, so `validate` warns and quotes back the alternative the split produced. Only the author knows which split was meant, and the commit-msg hook is the last moment they can still change it. For records already written, where validate can no longer reach, `ruled-out` and `context` annotate the line, and `guard` carries a `caveat:` line into the stderr block a PreToolUse hook routes back to the agent. The split itself moved out of `core/guard.ts` into `core/trailers.ts`, because four callers now need the same answer about one grammar and three of them are not the matcher. Record-Id: r-pipesplit Limit: test/dogfood.test.ts validates every record in this history, so a new violation class is only available if it rejects none of the 620 Ruled-out: values already written Ruled-out: Splitting on the last pipe instead of the first | this repository's own records refute the premise that reasons rarely contain a pipe: two of the three multi-pipe values carry it in the reason, so the change would break two correct records to fix one Ruled-out: Defining a backslash escape for the separator | that is a new grammar rather than a clarification -- parser, renderer and validator would all have to agree on unescaping, and it changes what a conforming record looks like, which needs sign-off this fix does not have Ruled-out: Making any value with more than one pipe a format violation | it invalidates 616005d and aa68a9a, whose extra pipe sits in the reason and whose split is already correct, and dogfood validates all of history Ruled-out: Having guard score both the first-pipe and last-pipe splits and keep the better one | it invents an alternative no author wrote and then reports it as recorded, which puts the silent wrong answer in a new place instead of removing it Ruled-out: Rendering every Ruled-out: value pre-split in the query output | it rewrites 620 lines of output to mark 3, and the annotation is only information where the split is actually in doubt Ruled-out: Warning on the code-span case rather than refusing it | a span that crosses the separator is provable rather than suspected, and this repository refuses what it can prove instead of hoping the author reads stderr Certainty: firm Blast: module Undo: easy Verified: the issue reproduced against 0.5.1 -- validate exit 0 with shape ok, guard matched:false at --threshold 0 against the real alternative and 0.75 against the truncated fragment, and a backslash before the pipe was kept and split anyway; the 620-value census was taken with the real parser over git log --all; 14 tests were run red first across trailers, guard, validate and query; after the fix trailers, guard, validate, schema, dogfood, squash, query, stale, index-db and cli all pass, dogfood included, so no past record was invalidated; typecheck clean, dist rebuilt, spec/verify.sh reports 26 fixtures, check-readme-numbers clean Unverified: whether any record outside this repository puts a pipe in an alternative without a code span -- that class is warned about, not refused, and the warning is only read by whoever is committing; core/inject.ts still renders Ruled-out: values verbatim, because annotating there would spend an entry's budget and could evict another record, so an agent reading injected context alone does not see the caveat CommitLore-Version: 2.0.0
Refuse the Ruled-out: split that provably went wrong, and report the rest
Minor rather than patch. Three things in this range change behaviour a caller can observe rather than only fixing a wrong answer: a same-second collision now reports for review with its content withheld instead of resolving to one declaration, garbage collection now removes a capture transaction that can never be staged, and `validate` refuses a `Ruled-out:` value whose code span crosses the separator. A patch number would tell an upgrader none of that had happened. The `Ruled-out:` change is the one worth reading before upgrading. It is a narrowing -- every value that conformed to 0.5.1 still conforms, checked against this repository's whole history -- but a record written elsewhere with a pipe inside a backtick span will now be refused where it used to pass silently. That record could never match the alternative it named, so the refusal is the point. The measured result is in the notes because it is the first number this project has for the question it exists to answer, and because the honest version of it took two runs: the first compared an unbounded `git log` against an 800-token projection and read the difference as a property of path scoping. It was confounded. Measured at equal budget the scoped route leads by 39.7 points and carries no retired records, and unbounded it recovers exactly what a whole-repository dump recovers -- so scoping costs nothing and the budget costs 10.6 points. The superseded run is kept with its conclusion retracted. Record-Id: r-rel060 Limit: the install one-liner in all four READMEs now points at a tag that does not exist until this is tagged, so the window between merging to main and pushing v0.6.0 is one where the documented install is broken Ruled-out: 0.5.2 | three observable behaviour changes and one narrowed acceptance rule are not a patch, and an upgrader reading the number would not go looking Ruled-out: Publishing the delivery figure in the README | the measurement is one corpus and one repository, and the README already carries more numbers than a first-time reader can weigh; docs/evidence.md and bench/ hold it Certainty: firm Blast: system Undo: costly Verified: readme, readme-order, readme-numbers, readme-positioning, manifest and compatibility-matrix pass at 107 with the new pins; check-readme-numbers.mjs exit 0 with the BENCH block byte-identical; spec/verify.sh OK at 26 fixtures; dist rebuilds with no diff and the bundle reports 0.6.0 Unverified: the install one-liner against the real tag -- that cannot be checked until the tag is pushed, and is the first thing to check after it is
Release 0.6.0
CommitLore — record lintTrailers: clean — 47 commits in Active constraints for the paths this PR touchesLimits (173)
Ruled out (381)
Truncated: 326 lines omitted — the comment hit GitHub's 65000 character limit. Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
devtomainfor the 0.6.0 release. 47 commits.Minor rather than patch. Three changes move behaviour a caller can observe:
Record-Idcollision reports for review with content withheld, instead of resolving to one declaration by an ordering that was effectively random (Two commits in the same second make context and stale resolve one record differently #350)capture gcremoves a transaction that can never be staged, andpending rm <nonce>exists (A capture that is never staged leaks its pending file, and gc is designed not to collect it #367)validaterefuses aRuled-out:value whose code span crosses the separator (Ruled-out: splits on the first pipe, so an alternative containing '|' is truncated and cannot match #372)The third is a narrowing — every value that conformed to 0.5.1 still conforms, checked against this repository's entire history by
test/dogfood.test.ts. A record written elsewhere with a pipe inside a backtick span will now be refused where it used to pass silently; that record could never match the alternative it named.Correctness
Supersedes:retired the record that is livecontextandstaleanswer differently about one recordhooks uninstallremoved one of the three hooksinitinstalls; the two left behind blocked every commit once the CLI movedvalidatereported everyduplicate-idtwice and counted it twiceHonesty
capture --helpsaid--diffdefaults to empty; it defaults to the staged diffmode: "suggest"is a host-side convention the core cannot enforce — ADR-0028 prices the alternative and declines itharvestpath, not the verified capture pipelinedocs/; three blocks stay because CI pins them thereMeasured
bench/DECISION-DELIVERY.md— how much of a repository's active decision set a route delivers before the first edit. At the shipping 800-token budget: 81.7% of path-attached active records, zero retired records delivered. Ordinarygit logfor the same path at the same budget reaches 42.0%, delivers 7 retired records, and spends more tokens.Unbounded, the scoped projection and a whole-repository dump recover the identical 2,047 of 2,217 pairs — path scoping costs nothing — for 741,429 tokens against 92,175,612 and 0 retired records against 7,322. The remaining 170 pairs are the ceiling the trust grader sets, not the scope.
This is delivery, not recovery: no agent ran, so it bounds what one could recover. One corpus, one repository. The error term is half-exercised — 7 superseded records, 0 expired — so "zero retired delivered" says nothing about expiry. It does not discharge ADR-0017's registered study.
The first run of this benchmark reached the opposite conclusion and was confounded; it is kept in
bench/with its conclusion retracted in place rather than deleted.Known window
The install one-liner in all four READMEs points at
v0.6.0, which does not exist until the tag is pushed. Between this merge and the tag, the documented install is broken.