Skip to content

Release 0.6.0 - #377

Merged
MongLong0214 merged 47 commits into
mainfrom
dev
Aug 1, 2026
Merged

Release 0.6.0#377
MongLong0214 merged 47 commits into
mainfrom
dev

Conversation

@MongLong0214

Copy link
Copy Markdown
Owner

Promotes dev to main for the 0.6.0 release. 47 commits.

Minor rather than patch. Three changes move behaviour a caller can observe:

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

#351 a note on a commit the history no longer reaches was served as active, and its Supersedes: retired the record that is live
#350 two commits in one second made context and stale answer differently about one record
#354 hooks uninstall removed one of the three hooks init installs; the two left behind blocked every commit once the CLI moved
#352 the commit-msg hook refused valid records in a shallow clone and on multi-block messages
#367 a capture that was never staged leaked its pending file permanently
#365 validate reported every duplicate-id twice and counted it twice

Honesty

#353 the plugin ships no CLI, and the next documented step needed one
#359 capture --help said --diff defaults to empty; it defaults to the staged diff
#341 mode: "suggest" is a host-side convention the core cannot enforce — ADR-0028 prices the alternative and declines it
#340 the commit skill taught the manual harvest path, not the verified capture pipeline
#344 the README moved its reference material into docs/; three blocks stay because CI pins them there
#364 two demo tests scanned the process-wide temp directory

Measured

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. Ordinary git log for 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.

MongLong0214 and others added 30 commits August 1, 2026 11:15
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
MongLong0214 and others added 17 commits August 1, 2026 14:32
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
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 47 commits in origin/main..d4be39de75139457205430531a61b3f9384dea21
Active constraints: 173 limits · 381 ruled-out · 98 warnings — from 205 records over 135 changed paths

Active constraints for the paths this PR touches

Limits (173)

  • r-rel060 e999b9d — 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
  • r-pipesplit b4fa571 — 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
  • r-delivpairedresult e9332a6 — one corpus, one repository, one query strategy per route; this is delivery, which bounds recovery from above, and no agent was run
  • r-delivarms b3f5692 — 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
  • r-delivpaired a0ecca3 — 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
  • r-delivmethod b97f3a6 — 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
  • r-deliv821 c2d3230 — one corpus, one repository, one query strategy per route; delivery bounds recovery from above and no agent was run
  • r-delivharness ccbb75c — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-secondtie 998bf18 — 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
  • r-dedupviol 18ad9c1 — 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
  • r-readmesplit344 7314a03 — 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
  • r-owntmproot 6543870 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-adrdated ff7946c — nothing enforces the header shape, so the next ADR can drop the date again
  • r-diffdefault 4ac8163 — 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
  • r-shallowlast 0913821 — the spawn still happens once per validate that has a dangling ref, which is the case where the answer is actually needed
  • r-exitonemeans 89f7af8 — 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
  • r-failopen abc54ea — 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
  • r-notereach 1e72a28 — 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
  • r-captureskill 6947cf9 — a skill loads into every agent's context, so a paragraph duplicating what the runtime prompt already prints is paid for on every session
  • r-heropolish f6144bc — README.ko.md still switches from 존댓말 to 해라체 below the hero; that split is older than this change and belongs to the restructure in README still carries the reference manual it should be linking to #344
  • r-pluginpath353 e364f3a — a plugin manifest has no way to add anything to PATH, so no plugin-side change can make the documented commands resolve
  • r-fetchowed 11f04b4 — config alone cannot separate a refspec that was fetched through from one that was only written, so the availability verdict cannot carry that distinction
  • r-guarddisclose 8a4d0c7 — 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
  • r-realoutput f9efea0 — 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
  • r-refspecfetch 936d206 — 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
  • r-actionsleak a6fbb4b — 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
  • r-actionsleak a6fbb4b — 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
  • r-entropyscope 077d0f6 — an unclaimed capability is indistinguishable from an unbuilt one, so a scope question left silent gets resolved later in whichever direction sells better
  • r-release051 19810d2 — the hook is written at install time, so no release repairs a repository that already has one; every release touching hook behaviour has to restate what does
  • r-heroinherit 89b13ac — a headline that implies detection commits the product to guard's numbers, and guard is an advisory measured at 22% recall
  • r-convertreadme e12c816 — a README claim about the default workflow is only true if the shipped skill performs it, and the skill currently requires the user to name CommitLore first
  • r-fieldreport 753f4e7 — this section reports one engineer's day on one repository; it is evidence that the mechanism works there, not a measured effect size, and the wording has to keep those apart
  • r-readmefinal 40aeae0 — a mutation oracle anchored on a claim that can become false will silently stop testing when the claim is removed; the needle has to be asserted present
  • r-recordgate335 a83ebe3 — a denylist cannot decide whether something is a record, because the keys nobody has claimed are unbounded; that question needs the vocabulary, and the two must not be answered by one filter
  • r-recordgate335 a83ebe3Verified: in a release note is indistinguishable from Verified: in a record, and no context signal separates them without risking real records
  • r-draftfirst329 0506a5d — a usage error that names the wrong input costs an invocation and points the reader away from the fault; ordering is part of the message
  • r-release050 ad402c7 — the hook is written at install time, so a corrected release never reaches a repository that already has one; every release fixing hook behaviour has to say what repairs an existing install
  • r-uninstall1123 4ddac0d — the installers write five agent configs, not the four the ticket's measured inventory lists; the fifth is Windsurf at .codeium/windsurf/mcp_config.json
  • r-uninstall1123 4ddac0d — opencode's entry is shaped differently from the other three -- the command is an array -- so one recogniser cannot serve all of them
  • r-uninstall1123 4ddac0d — a checkout is 1366 files at this head, not the 1206 the ticket measured at 6e1d46d; any assertion bound to that count is stale
  • r-winpath1127 bdf4ac0 — the stub is written to .git/hooks at install time, so a repository installed before this fix keeps the old text and must re-run commitlore hooks install; installing a corrected release is not enough
  • r-winpath1127 bdf4ac0${dir%/*} returns its input unchanged when no separator remains, so a loop that tests for emptiness never terminates at a drive root
  • r-winpath1127 bdf4ac0 — neither dirname nor ${var%/*} finds a parent in a backslash-separated path; both answer .
  • r-compat1122 e7d8516 — a non-empty guard does not detect deletion; each table's row keys have to be asserted as a set or the statement can silently shrink to one row
  • r-compat1122 e7d8516 — substring comparison hides a narrowing -- ./ is inside ../ and Edit|Write is inside Edit|Write|MultiEdit|NotebookEdit -- so cells are compared as their rendered form
  • r-compat1122 e7d8516 — a sentinel containing \0 makes git treat the file as binary, which costs it diff, blame and log -p permanently
  • r-compat1122 e7d8516 — the plugin path needs bash, because scripts/commitlore-run.sh carries a #!/bin/bash shebang, and no install script checks for it
  • r-muslbullet1126 04ac181 — this ticket owns four bullets and not the tests that read the section around them, so a check that breaks here means a region was taken that was not allocated
  • r-t1120nodeinst 14deeb4 — git and node are hard prerequisites now, so a host without them installs nothing and says which one is missing
  • r-t1110policy 9e7b37a — only a repository-local policy file is read -- PRD-F13 requirement 11 permits either one location or a stated precedence, and an ambiguous precedence is worse than a missing feature
  • r-gateb3rev a2e38b9 — the shipped install.sh downloads a platform asset, so no document may describe it as Node-only until the installer itself changes
  • r-rel041notes 71efe1f — 0.4.1 makes the installer honest about a verification it cannot complete rather than fixing the kill, so an upgrading user may still see the unverified message instead of a version
  • r-rel040pins b76c40b — the pin names a tag that does not exist until the tag is pushed; between this merge and that push the documented command refers forward
  • r-rel040notes 5d57a72 — the 26.3-point density gap quoted in the notes is measured at this head and will drift with merge volume; it is illustrative of the denominator problem rather than a stable figure
  • r-gcwiring f21f28e — the guard against this class is four CLI-level tests; nothing structurally prevents a future subcommand from colliding with a parent option again
  • r-flake221fix 2b21ed9 — checkInjectRuntime ENOENT does not block init
  • r-density183denom 62ee7a4 — density row must name both denominators; a rate without a named population is the defect class "check must state what it could not see"
  • r-lb0xl89a 236229e — the static contract uses explicit placeholder text for TRANSCRIPT and DIFF rather than omitting those sections, because the prompt text references them by name
  • r-c44a1edb 71f5197 — src/core/pending-gc.ts -- gc must never remove a staged or applied file regardless of expiry; T-1018 post-commit may still finalise them
  • r-0ll5sxk0 2853a22 — consumption happens after commit succeeds, exactly once; consuming earlier loses the record on failed commits, consuming twice lets one record attach to two commits
  • r-t1009list 778638c — the list is still maintained by hand, so it will need editing for every future tool; that is the cost of catching an unintended one
  • r-t1009stage b5fcf4e — the nonce pattern check bounds what a caller can send, but a caller holding a valid nonce for its own repository can stage repeatedly until the record is consumed
  • r-t1005gates 15421c0 — policy identity is compared as a hash, so a policy edit that produces the same hash is indistinguishable from no edit
  • r-t1016svg 321c6f1 — byte-exactness is verified on this platform; a different platform's Node could in principle render differently, and nothing here proves it does not
  • r-t1006cli d22580b — the command composes the phases in one process, so a crash between verify and stage leaves a verified pending record that only garbage collection will clean up
  • r-t1008mcp ab00b54 — src/mcp/server.ts: readOnlyHint must be false for verify_capture — the tool writes verification results to the pending transaction
  • r-t1007mcp b6ef112 — commitlore_prepare_capture uses readOnlyHint: false because it writes a pending transaction
  • r-t1013verbose205 294ec82 — --verbose only selects the formatter; it does not change runInit logic, step order, exit codes, or --json output
  • r-t1022sig e0c641d — the first pushed attempt asserted one header string in the test and built another in the formatter; CI caught the mismatch and the formatter was aligned to the asserted string, which is the one that states the measured figures on the output surface
  • r-t1022sig e0c641d — focused-test evidence for this change is CI's, not local; test/guard.test.ts reports zero tests and stalls on this machine at dev with no changes applied
  • r-t1024bc 023f6d9 — response shape is exactly five fields per CEO amendments and ADR-0020 confidence-separation constraint | adding a sixth field or letting context inherit guard_confidence violates the acceptance criteria
  • r-t1021known 8dfffc1 — the figures are measured against one archived 417-decision corpus, which is deliberately hard and is not deployment prevalence
  • r-t1011demo 1c0fc0c — the scene is one fixed pair of decisions, so it demonstrates the mechanism rather than measuring how often it matters
  • r-t1001pend 0f5ef32 — the test proves monotonic transitions but cannot prove absence of TOCTOU between read and rename on a loaded filesystem; atomic rename is the kernel-level guarantee
  • r-t1001pend 0f5ef32 — the first attempt's Record-Id used hyphens, which the r-[a-z0-9]{6,} format rejects; both the lint action and the dogfood test caught it
  • r-t1020desc dd12b42 — the test asserts on the exact precision and recall figures; a future re-measurement changes both the description and the test
  • r-t1020desc dd12b42 — the first attempt's Record-Id used hyphens, which the r-[a-z0-9]{6,} format rejects; both the lint action and the dogfood test caught it
  • r-initresult204 ea4a08e — --verbose flag not wired yet (T-1013)
  • r-t1030diag 344ada0 — the heuristic uses a regex on the first line of stderr; an error that prints no stack frame and no "not found" string will be reported as cause unclear even if a human could classify it
  • r-pin030readme 504b54e — install.sh must already support tag-based download for the one-liner to work; verified that the URL resolves to a tagged tree
  • r-gate2amend a4bdc42 — nothing mechanically enforces that a future heading keeps prerequisites and ordering in separate fields; the separation is stated and reviewed, not checked
  • r-fix191amb cb94448 — the same-message test still passes by accident of collectRecords returning one record per commit; the divergent-notes test is what exercises the actual suppression path
  • r-fix187val 40f2436 — the tip-scan adds one full-history git-log call per range invocation; acceptable for a lint-time check but visible in benchmarks at scale
  • r-notes030 a289ca5 — the density denominator is named here and in the handoff, not in the harness that emits it, so the next run reproduces the same ambiguity
  • r-hero172a bc0d971 — Stale-exposure benchmark is one corpus, one query, and one pinned embedding model at a fixed two-record budget
  • r-bench161 b9b788e — digest fallback proves identical harness code at HEAD, not existence of the recorded commit
  • r-econ127a 40e197d — no task in the current 8-task pool passes Make benchmark task qualification two-sided (4–5 of 6) #109 qualification
  • r-dupsucceed 6f77fcf — supersession is resolved within one repository's history, so a record superseded in a fork that was never merged still grades as current here
  • r-dupsuccorder f46c02d — a successor before a later duplicate cannot resolve that later collision
  • r-dupsucc729 5a6b238 — published dev history cannot be rewritten
  • r-densitymerge135 490b2bb — the density-only path must not contend with a wall-clock benchmark
  • r-densityci135 e81cb1f — a published density result must be recomputed from its recorded source history in CI
  • r-cpaa134 f6f6d03 — the floor excludes the model's generation cost, so a reader comparing this against a hosted service's bill is comparing a part against a whole
  • r-valdup145 bcb9563 — the same-message check sees only the message, so two commits each declaring the same id separately are still caught by the reference check rather than here
  • r-convtrail150 57e89d2 — the denylist answers a different question from isRecordKey's allowlist, so a conventional trailer this protocol later claims would need removing from one and adding to the other
  • r-epipe2026 d9ee9ff — spawnSync may report EPIPE after git exits while its input pipe is being written
  • r-f61curve f05a3fc — the frozen 30-label corpus gives the archived 3/8 precision a 95% Wilson interval of 13.7%–69.4%
  • r-scale131 3f0f4a5 — production measurements retain every previously published fixture size
  • r-benchlock131 79f7ae2 — two runs launched with different node binaries or through a wrapper that does not name node in argv are still not detected
  • r-doctorepipe 0420f5c — the new deterministic tests exercise evaluateInjectRun with a synthetic spawnSync result rather than forcing the live race, because no payload this check sends is large enough to make the write block deterministically the way an artificially large one does in the reproduction above
  • r-init107 f485f07 — the generated dist artifacts are rebuilt from TypeScript source
  • r-survsplit e73aed5 — path-reachability is measured against git's rename detection, so the figure moves with git's similarity threshold rather than with anything here
  • r-squash132 c19160e — GitHub's server-side squash button runs no local hook, so that path still loses records
  • r-be140cost 8c01bd5 — no per-turn provider token ledger or observed avoided-work cost exists yet
  • r-noise142 aa3d1f1 — exposure uses a fixed-seed corpus of two applicable records and realistic unrelated records at five sizes
  • r-probepath 51f6446 — the probe still only runs a command it recognises, so a hand-edited but equivalent hook reports not-checked rather than a verdict
  • r-readme129 ab5f210 — the break-even rests on tokens estimated from bytes at the product's own four-characters-per-token constant, so it moves with that assumption
  • r-doctorprobe ed94491 — the probe runs only a command it recognises, so a user who hand-edits the hook into an equivalent but different form gets not-checked rather than a verdict
  • r-m4basis 5e2d2cb — the guard question stays unanswered until the exposure instrument is verified and M4 is rerun on it
  • r-m4withdraw e5f9b73 — the guard question is now unanswered rather than answered null
  • r-readmeux1 b664205 — interactive record building does not exist, so the honest answer is still "an agent writes it or you do"
  • r-rel021a a79e350 — v0.2.0 remains on the remote with no release attached
  • r-expreadme1 9e69abe — bench/VERDICT-M4.md still cites the Fisher figure; the two disagree until the verdict records why the number was withdrawn from the README
  • r-expomerge1 d6ad014 — M4's existing rows have no exposure field and must read as unknown, not as not-exposed — backfilling by inference would erase the finding
  • r-f61a2c 9114cf0 — the matcher remains deterministic and lexical; no embedding or semantic service is available to distinguish paraphrases
  • r-rdme96a 9c9371c — scripts/check-readme-numbers.mjs's withdrawal-notice and stray-statistic checks constrain what can appear outside the (absent, here) generated benchmark block — re-checked after every edit, not just at the end
  • r-init96a 913c7e3 — doctor's own exit-code contract treats warn as non-fatal by design (SPEC §10, commitlore-setup skill) — init deliberately diverges from it for its own summary, and that divergence is the one thing most likely to look like a bug on a future read of this diff
  • r-fix92dupid 7f41a6e — cross-references between two blocks declared by the same commit (a Follows:/Supersedes: naming a sibling block's id) are still reported as dangling rather than resolved against the sibling -- unchanged from before this fix, and called out in validate.ts's own comment as future work
  • r-fix93pkg 9c4a396 — package.json remains a development artifact (build, typecheck, dependency floor) -- it is not read as a distribution manifest by anything in this repository
  • r-relinstall c6e1d04 — never tested against the real GitHub release infrastructure (no release exists yet — that is the owner's action) — verified against a locally built SEA binary, a hand-made SHA256SUMS, and a local HTTP server standing in for GitHub's release-asset redirects, which is everything this repository lets a change verify before a tag exists.
  • r-distrace88 d118a73 — the fix insulates bench-ablation.test.ts from the race; it does not remove the underlying design (four test files independently, redundantly rebuilding one shared dist/ in their own beforeAll). A fifth file doing the same thing, or a future check elsewhere that also depends on dist/'s mid-run stability, can still race the same way.
  • r-parsemulti 6d39d25parse has no git-commit context (no sha, no notes mirror) — its identityCollision check is local to the one message being parsed and cannot detect a Record-Id that collides with something already committed elsewhere in history the way context's fold does.
  • r-multirec01 92aeb24 — parseRecordBlocks only recognizes a non-final block by its declared Record-Id, so an unidentified inherited record beyond the first stays recoverable in the plan that computed it but not in a later re-parse of stored text; squash-preserve orders unidentified blocks last so the common case (at most one) is unaffected.
  • r-multirec01 92aeb24 — multi-block reference checking (Follows:/Supersedes:) does not resolve one block's reference against a sibling block declared by the same commit; each block is still checked against every earlier commit in history.
  • r-issue59a1 21dd96c — three score bands over 8 firings is still a small-sample report; issue Guard fires wrong 5 times out of 8 — precision 37.5% at the shipped threshold #61 item 5 tracks corpus growth as a separate concern from this wording fix
  • r-exit065 e545dee — any new command's exit codes must be drawn from SPEC §10, not invented locally
  • r-fix70a1 d707fc7 — one encoding layer and explicit lexical forms in the four published languages; semantic paraphrases, nested encodings, and split payloads remain outside coverage
  • r-shwt66 5efa206 — git rev-parse --git-path may return a repository-relative path, so resolve it against cwd
  • r-merge66 40e7987 — Generated dist files were resolved only by npm run build and npm run bundle
  • r-fix760 fb8ba45 — Git remains the authority on trailer recognition; diagnostics must not loosen the parser
  • r-refint74 572f573 — validate cannot perform conservation checks because it has no before state
  • r-warn75 24c7cc8 — exit-code semantics remain owned by guard's exit 2 means blocked; everywhere else in the same CLI exit 2 means bad usage #65
  • r-shallow66 60a8659 — a depth-1 clone can only inspect its reachable commit history
  • r-doctor72 996bcde — generated dist artifacts must come from npm run build and npm run bundle, not a hand merge
  • r-fix067 a915af0 — PreToolUse hook failures must always exit 0 and never change stdout's hookSpecificOutput contract
  • r-fix063 0b8c496 — doctor performs remote probes; an unreachable remote reports could not verify instead of ok
  • r-det58p 1edccdf — deterministic results must identify a committed harness, corpus, binary, run, and machine
  • r-det058 695cdf6 — the suite must need no model, agent, network or uncommitted benchmark input
  • r-fix053 ecc4b90 — QueryResult.notes remains repository-level availability and is independent from whether one record was mirrored
  • r-fix055 43b40f8 — harvest-verify makes no model call, so semantic entailment is outside its contract
  • r-fix054 664d4e2 — notes-only metadata must survive folding; a mirror is one record, not two
  • r-fix056 55cb8bc — blocked output may retain only validated structural values that cannot carry prose
  • r-7a3e91 cf859e4 — better-sqlite3 stays external because it is native — the bundle degrades to --no-index without it, which only works because r-6f2a08 made that load lazy first
  • r-6f2a08 4c2d432 — esbuild cannot follow createRequire(import.meta.url)('x'), so any dependency reached that way stays external no matter what the bundle config says
  • r-9c07e2 9c4d25a — the plugin still needs Node for the CLI — the protocol does not, but guard, the index and the MCP server do (T-706 · Bundle the CLI as a single file — run from a clone alone #38)
  • r-5a29f7 7727944 — guard's false-alarm rate on real agent output is 5 in 25 on diffs; an earlier pass reported 0 in 10 on curated proposals, which was the optimistic case rather than a regression since
  • r-9c2f74 d653153 — the ablation arms cannot discriminate on these fixtures -- no-grade and no-lifecycle are byte-identical to the treatment in 9 of 10 tasks, because the seeds carry one reconstructed record and one task with a lifecycle trailer between them
  • r-9c2f74 d653153 — the harness assembles its own projection rather than calling the shipped injector, so what is measured is the harness's rendering of the records, not src/core/inject.ts (issue B-08 · Replace the benchmark harness injector with the actual src/core/inject.ts #36)
  • r-4a8e15 49e12c7 — git's grammar requires a subject before a trailer block, so a serialized block is not by itself a parseable message
  • r-1e7b40 c3ecbb6 — the merge is the only moment the branch's commits are still reachable, so this has to be right the first time or the records are gone
  • r-6e1a72 5e09846npx commitlore is the first thing a reader will try, and it fails until the package is published
  • r-2b8f45 0adcaf5 — a matcher that flags real work gets uninstalled, so the false positive rate is the binding constraint, not detection
  • r-9f3b16 6b13589 — git decides rename lineage by content similarity, so any claim about following renames is a claim about a threshold, not a guarantee
  • r-4e9c72 a7a7e26 — the index is derived, so nothing about its state can make the tool give a wrong answer -- only a slower one
  • r-8c4a17 f2ab0c2 — Record-Id is single-valued, so a merge that inherits several records has no well-formed way to declare them in the message
  • r-8c4a17 f2ab0c2 — a verifier that accepts near-miss citations verifies nothing, so normalisation cannot grow past whitespace
  • r-1b7d94 736ef92 — git reads ambient configuration, so a suite that does not neutralise it is testing the developer's machine as much as the code
  • r-7e5f02 e5f5e00 — npm installs through an engine mismatch, so the ecosystem's own signal cannot be relied on to stop anything
  • r-9a5e17 6d68703 — five workers on one repository share npm test and tsc, so file ownership alone does not prevent one worker from "fixing" another's half-written code -- verification scope had to be split too
  • r-4d8a13 b1034ca — the same field carries two shapes, so any near-miss between them resolves to whichever branch is looser unless the looser one excludes the shape deliberately
  • r-1f4b26 65f01c7 — this repository ran under two retired names, so its early records cannot validate against the current vocabulary and a whole-history check would be permanently red
  • r-7f0e39 76f3f2d — literal substitution only catches the exact strings you list, so the same term written with a different separator survives
  • r-5a8c04 c46a577 — git owns the definition of a trailer block, so any behavior we cannot get from interpret-trailers is behavior we must not invent
  • r-9d31b7 4ac6e30 — the example lives in four translated files, so any fix that is not mechanically enforced will drift again on the next edit
  • r-c0f4e2 3d249cd — npm gitlore is held by an active same-domain CLI, so the owner's first-choice name was not available
  • r-b2e7f1 00d348d — Parsing must delegate to git interpret-trailers -- reimplementing the block rules would drift from the rest of the git ecosystem
  • r-a8f3c1 ef48843 — Rename must land before any code exists -- after 27 tickets it would touch spec, fixtures, index, hooks and every doc

Ruled out (381)

  • r-rel060 e999b9d — 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
  • r-rel060 e999b9d — 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
  • r-pipesplit b4fa571 — 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
  • r-pipesplit b4fa571 — 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
  • r-pipesplit b4fa571 — 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
  • r-pipesplit b4fa571 — 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
  • r-pipesplit b4fa571 — 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
  • r-pipesplit b4fa571 — 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
  • r-delivpairedresult e9332a6 — 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
  • r-delivpairedresult e9332a6 — 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
  • r-delivpairedresult e9332a6 — 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
  • r-delivarms b3f5692 — 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
  • r-delivarms b3f5692 — 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
  • r-delivarms b3f5692 — 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
  • r-delivpaired a0ecca3 — 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
  • r-delivpaired a0ecca3 — 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
  • r-delivpaired a0ecca3 — 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
  • r-delivmethod b97f3a6 — Running ADR-0017's registered pilot instead | it needs two independent annotators and forty agent runs, while No evidence that a fresh agent recovers the decisions a repository holds #343 asks a narrower question that this repository can already decide
  • r-delivmethod b97f3a6 — 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
  • r-delivmethod b97f3a6 — Writing the method after the first run | a definition chosen once the numbers are visible cannot be told apart from one fitted to them
  • r-deliv821 c2d3230 — 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
  • r-deliv821 c2d3230 — 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
  • r-deliv821 c2d3230 — 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
  • r-delivharness ccbb75c — 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
  • r-delivharness ccbb75c — 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
  • r-delivharness ccbb75c — 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
  • r-delivharness ccbb75c — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-gcunstageable 5cd6b8f — 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
  • r-secondtie 998bf18 — 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
  • r-secondtie 998bf18 — 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
  • r-secondtie 998bf18 — 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
  • r-secondtie 998bf18 — 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
  • r-secondtie 998bf18 — 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
  • r-dedupviol 18ad9c1 — 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
  • r-dedupviol 18ad9c1 — 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
  • r-dedupviol 18ad9c1 — 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
  • r-readmesplit344 7314a03 — 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
  • r-readmesplit344 7314a03 — 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
  • r-readmesplit344 7314a03 — 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
  • r-readmesplit344 7314a03 — 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
  • r-readmesplit344 7314a03 — 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
  • r-owntmproot 6543870 — 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
  • r-owntmproot 6543870 — 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
  • r-owntmproot 6543870 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-asksbeforestage 81bc919 — 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
  • r-adrdated ff7946c — 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
  • r-adrdated ff7946c — 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
  • r-diffdefault 4ac8163 — 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
  • r-diffdefault 4ac8163 — 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
  • r-shallowlast 0913821 — 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
  • r-exitonemeans 89f7af8 — 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
  • r-exitonemeans 89f7af8 — 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
  • r-exitonemeans 89f7af8 — 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
  • r-failopen abc54ea — 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
  • r-failopen abc54ea — 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
  • r-failopen abc54ea — Having the two hooks say nothing at all | a hook that captured no record and reported nothing is the silent-success shape doctor's own fix breaks git fetch and git pull, then reports ok #63 and inject --hook-input is byte-identical on malformed input and on no-records: silent fail-open #67 were, and the state it hides is one command away from being fixed
  • r-notereach 1e72a28 — 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
  • r-notereach 1e72a28 — 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
  • r-captureskill 6947cf9 — 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
  • r-captureskill 6947cf9 — 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
  • r-heropolish f6144bc — 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
  • r-heropolish f6144bc — 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
  • r-pluginpath353 e364f3a — 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
  • r-pluginpath353 e364f3a — 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
  • r-pluginpath353 e364f3a — 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
  • r-fetchowed 11f04b4 — 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
  • r-fetchowed 11f04b4 — 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
  • r-guarddisclose 8a4d0c7 — Removing the sentence without adding the measured figures | the ADR requires both, and an empty description discloses nothing about a 22% recall
  • r-guarddisclose 8a4d0c7 — 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
  • r-realoutput f9efea0 — 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
  • r-realoutput f9efea0 — 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
  • r-refspecfetch 936d206 — 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
  • r-refspecfetch 936d206 — 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
  • r-actionsleak a6fbb4b — 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
  • r-actionsleak a6fbb4b — Keeping the fallback and pinning a version | there is no published version to pin, so this would have been a slower way to fail
  • r-actionsleak a6fbb4b — 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
  • r-entropyscope 077d0f6 — 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
  • r-entropyscope 077d0f6 — Deferring it to a later milestone | a deferral is a promise with no date, and this question has now been raised in two reviews
  • r-entropyscope 077d0f6 — Leaving the README silent on it | silence is what produced the question twice
  • r-release051 19810d2 — Waiting for 0.5.2 to carry index ingests any key: value line as a trailer; doctor reports 106 records where git has 0, and context serves commit subjects to the agent #335 | false decision context reaching an agent is the inversion of the product claim, and a stable release should not hold it
  • r-release051 19810d2 — 0.6.0 | no command is added and nothing breaks; calling it minor would hide a correctness fix behind a feature number
  • r-release051 19810d2 — Filtering Verified: because a report listed it as noise | it is vocabulary, and separating a release-note use from a record use needs a guess that can discard real records
  • r-heroinherit 89b13ac — Keeping "Stop re-reviewing the same bad idea" as the hero | it names a verdict on ideas rather than a lifecycle on decisions, and any review tool could carry it
  • r-heroinherit 89b13ac — "Never let agents make the same mistake twice" | guard cannot support a never, and the sentence sells detection this product does not perform
  • r-heroinherit 89b13ac — Leading with "decision authority" alone | precise in the architecture sections, and not a phrase a first-time reader converts on
  • r-convertreadme e12c816 — Adding "Keep coding normally" now | the commit skill still triggers on explicit requests and teaches harvest; shipping the sentence first repeats the doc-ahead-of-code failure this session fixed twice
  • r-convertreadme e12c816 — Writing testimonials to fill the social-proof section | one real field report exists, and fabricated or padded quotes would cost the claim honesty that is this project's strongest asset
  • r-convertreadme e12c816 — Moving the benchmark block and protocol detail out of the README now | the generated block is byte-compared by check-readme-numbers and the guard figures are asserted inside the limitations slice; the move is worth doing and is worth its own change
  • r-convertreadme e12c816 — Dropping "decision authority" everywhere | it is the right term in the architecture sections; it is only the wrong term for a first screen
  • r-fieldreport 753f4e7 — Presenting the 7.4s index and 768 commits as product benchmarks | they are one machine's numbers on one corpus, and bench/ is where measured claims live under ADR-0018's provenance rules
  • r-fieldreport 753f4e7 — Paraphrasing his account in the product's voice | the credibility is that somebody outside the project said it, and paraphrase throws exactly that away
  • r-fieldreport 753f4e7 — Leading the README with this | the opening claim is the product's own and should stay first; this belongs where a reader who is already interested asks what it looks like in practice
  • r-readmefinal 40aeae0 — Rewriting the bullet to say Windows is supported | the compatibility document owns the support statement, and a second copy in four READMEs is the duplication the ownership map exists to prevent
  • r-readmefinal 40aeae0 — Checking only the English README | the contradiction shipped in four languages, and a reader sees one of them
  • r-readmefinal 40aeae0 — Asserting every host row against every README line | only the supported direction is decidable from the table; a host the table calls undecided may honestly be described either way
  • r-recordgate335 a83ebe3 — Adding the observed noise keys to the denylist | ax, sha256 and the rest are one repository's accident, and the next repository invents different ones
  • r-recordgate335 a83ebe3 — Requiring Record-Id: to make a block a record | stale needs it for identity, but a commit carrying only Limit: is a record SPEC recognises and dropping it would lose real context
  • r-recordgate335 a83ebe3 — Inferring intent from position or neighbouring lines | that is guessing, and the failure mode is discarding a record somebody wrote on purpose
  • r-draftfirst329 0506a5d — Reporting all missing options at once | it would say the draft is unparseable and the transcript missing together, and the second is not true until the first is fixed
  • r-draftfirst329 0506a5d — Changing harvest --draft to match | it already resolves the draft first, verified by running it against the same prose rather than by reading the dispatch
  • r-release050 ad402c7 — 0.4.2 | a new command and a platform support claim are not a patch, and calling them one hides both from anyone reading version numbers to decide whether to upgrade
  • r-release050 ad402c7 — 1.0.0 | nothing here breaks an existing install, and the milestone that closed is Gate B rather than a stability commitment
  • r-release050 ad402c7 — Describing Windows as supported without the caveat | the word is the one most likely to be read as more than it is, and the release that earns it is the right place to bound it
  • r-uninstall1123 4ddac0d — Removing the entry whose key is commitlore | it takes a server the user named that way and the other install's entry on a two-install machine, and neither loss is reported
  • r-uninstall1123 4ddac0d — Parsing the TOML config and writing it back | the round-trip reformats the whole file, which is the "never reformat beyond the one entry removed" this ticket forbids
  • r-uninstall1123 4ddac0d — Rewriting a config that failed to parse | the failure mode is losing a config we were only supposed to edit one key of
  • r-uninstall1123 4ddac0d — Removing the Claude Code plugin cache | it is thousands of files this command did not write, keyed by plugin version; naming the step is the honest boundary
  • r-winpath1127 bdf4ac0 — Normalising only the recorded root at write time | it would fix new installs and leave every existing repository comparing a stored win32 string against pwd -P output
  • r-winpath1127 bdf4ac0 — Loosening the containment match so the legitimate bundle runs | that trades Security: commitlore.bin and COMMITLORE_BIN are executed, and the env path lacks the guard the config path has #71's property for a working hook, which is exchanging one defect for another and is what this ticket's forbidden scope names
  • r-winpath1127 bdf4ac0 — Renaming the containment helper to match its new signature | T-1125's invariant asserts the old identifier as its proxy for the property surviving, and that test belongs to another ticket
  • r-winpath1127 bdf4ac0 — Fixing the stub and leaving doctor's mirror alone | the mirror reports no problem in precisely the state where the hook is dead, so the check that should warn would keep saying healthy
  • r-compat1122 e7d8516 — Carrying the README's musl reason into the matrix | it cites glibc-only binaries, and there are no binaries; repeating it would document a mechanism that no longer exists
  • r-compat1122 e7d8516 — Reading the gate's removal as musl support | absence of a check is not evidence of working, which is why it was executed on two architectures instead
  • r-compat1122 e7d8516 — Claiming musl as a class from one image | alpine:3.21 is what ran, so other musl distributions are undecided rather than promised
  • r-compat1122 e7d8516 — One prerequisite column covering both install paths | the plugin path checks nothing, so a shared column would imply an enforcement a user does not get
  • r-compat1122 e7d8516 — Asserting the documented install command as its own string | it is <plugin>@<marketplace>, so deriving it from the two manifests makes a rename of either fail here
  • r-muslbullet1126 04ac181 — Replacing the bullet with "musl is supported" | T-1122 owns the compatibility statement, and a second summary in the README is the duplication the ownership map exists to prevent
  • r-muslbullet1126 04ac181 — Removing the Windows bullet beside it | it is still true, test/readme.test.ts uses its exact wording as a mutation oracle, and T-1124 owns that claim
  • r-muslbullet1126 04ac181 — Folding this into T-1122's pull request | that ticket's forbidden scope is one pointer line per README and nothing else; widening it to fix an adjacent owner's debris is how single-writer discipline decays
  • r-muslbullet1126 04ac181 — Merging T-1122 first and cleaning up afterwards | two ordinary merges are not transactional, and the window where dev contradicts itself has no upper bound
  • r-nobin284 ab2f08f — Keeping the binary classification arm for the installer's wrapper | the wrapper is a shell script that execs node, so trusting it by name would re-admit any extensionless executable called commitlore while the bundle it runs is the thing with an interpreter to check
  • r-nobin284 ab2f08f — Relaxing the dogfood Evidence check to allow a missing path | it exists to catch a citation nobody can follow, and dropping the check to accommodate a deletion would retire the guarantee instead of correcting the question it asked
  • r-nobin284 ab2f08f — Leaving src/core/paths.ts alone because ADR-0026's inventory did not name it | it was the only remaining code that read files out of a compiled artifact, so leaving it would have left the invariant false while the test asserting it passed
  • r-nobin284 ab2f08f — Keeping /dist/commitlore in gitignore in case a binary returns | an ignore rule for an artifact nothing produces is a claim that something might, and the ADR says otherwise
  • r-pending311 4985a3a — Extending capture gc to also list what it would remove | gc answers what is collectable, and the transaction a caller cares about is usually the one gc will never touch
  • r-pending311 4985a3a — Storing stale in the transaction file | it is a fact about HEAD now, so a stored copy is wrong the moment HEAD moves and right only when it is redundant
  • r-pending311 4985a3a — Changing expires_at so verified transactions expire | it silently deletes work a caller may still stage, and this issue asked to see the state rather than to change the retention rule
  • r-pending311 4985a3a — Resolving an ambiguous prefix to the newest match | it guesses at the one moment the caller has proved they do not know which transaction they mean
  • r-query307 3cf3c58 — Testing the working tree with stat | a deleted file has real history and real records, so stat would report a missing path for one of the cases the diagnostic exists to distinguish
  • r-query307 3cf3c58 — A distinct exit code for a path that is not in history | context is on the hook path, where a nonzero exit is a failure to fail open around, and the caller who needs to branch has a diagnostic to read
  • r-query307 3cf3c58 — Emitting the diagnostic unconditionally from runQuery | the PreToolUse path queries a file that is often being created, so it would fire on every new file and break the hook's silence contract
  • r-query307 3cf3c58 — Searching for the nearest rename alias rather than the nearest ancestor | the alias walk is what already ran and found nothing, while the ancestor probe is bounded at four parents and answers the question that actually bit the reporter
  • r-capture309 5a9655d — Printing the reasons without unifying the two draft paths | the envelope path produced no reasons to print, so the output would still have been silent for the reported input
  • r-capture309 5a9655d — Keeping the envelope path unchecked and letting the verifier speak for it | the verifier checks whether a citation holds, not whether a field exists, so an unknown field is invisible to it by design
  • r-capture309 5a9655d — Defaulting the diff to the empty string and documenting that --diff is required | it makes the ordinary invocation useless and contradicts prepare, which needs no --diff to bind the same bytes
  • r-capture309 5a9655d — Comparing hashes leniently so an empty diff passes | the binding exists so a draft cannot be verified against sources other than the ones prepared, and loosening it removes the guarantee rather than the defect
  • r-harvest310 c055467 — Making the plain harvest message conditional on whether a diff exists | it would make the prescription accurate by narrowing it, leaving the contract still unreachable in the case that matters
  • r-harvest310 c055467 — Emitting the contract only when the transcript is also absent | that is what harvest --prompt-only refuses to print the contract without a transcript and a diff #229 landed and what this issue reports as insufficient, since a caller usually has a transcript before they have a diff
  • r-harvest310 c055467 — Keeping the old assertion and adding a new one beside it | two assertions requiring opposite outputs for one input is not a test suite, it is a coin toss
  • r-hooks296 b701559 — Making hooks install re-validate only when the hook file is unchanged | recordBinPath already runs every time, so the short-circuit was never the cause and gating on it would have left the wrong value being rewritten
  • r-hooks296 b701559 — Falling through to commitlore.node in the stub | it changes the file that carries install-root containment, and a stub already written in an affected repository is not re-read, so it would repair nobody who has the problem today
  • r-hooks296 b701559 — Having doctor --fix repoint the config itself | once the prescribed command works, the prescription is true; a second repair path would leave two places able to write the same key
  • r-hooks296 b701559 — Recording resolve(argv[1]) with a warning when it does not exist | the hook cannot act on a warning it never sees, and a recorded dead path is what stops resolution at the first step
  • r-idxhelp303 cd2ac79 — Deleting the better-sqlite3 mentions in index-db.ts as well | those comments explain why node:sqlite was chosen, and removing the reason leaves a future reader wondering whether the native option was considered
  • r-idxhelp303 cd2ac79 — Asserting the package name appears nowhere in src at all | that forbids the explanation along with the defect, so the check reads only lines that reach a user
  • r-idxhelp303 cd2ac79 — Naming node:sqlite in the message | the failure a user hits is that the binding is unavailable, and the module name adds nothing they can act on
  • r-t1120nodeinst 14deeb4 — Hardcoding the release version in install.sh as the default | it would add a bump obligation to every release and a stale default between a tag and the bump; git ls-remote resolves the newest tag with no API token and no rate limit
  • r-t1120nodeinst 14deeb4 — Defaulting to a branch when no tag is given | installing a moving target is what pinning exists to prevent, and the release gate already forbids a dev-referencing one-liner
  • r-t1120nodeinst 14deeb4 — Writing the wrapper with cp over the destination | an in-place overwrite of a file that may be executing is the defect that forced a same-day patch release; the wrapper is written beside the target and renamed
  • r-t1120nodeinst 14deeb4 — Letting post-install verification decide the exit code | that was the other half of the same defect, where a killed --version became the installer's exit status; verification now retries once and reports, and an install that succeeded exits 0
  • r-t1120nodeinst 14deeb4 — Editing the shell profile when the wrapper directory is not on PATH | an active record on this file rejects it, and printing the line is what this script does about PATH
  • r-t1120nodeinst 14deeb4 — A test-only environment hook to force a verification failure | a source repository whose bundle exits non-zero is a real broken release and needs no seam in shipped code
  • r-t1120nodeinst 14deeb4 — Checking for git with command -v alone | a git that cannot execute is as useless here as a missing one, so the check runs git --version and catches both
  • r-mcpdraft291 d4444d5 — Changing the harvest contract to emit a bare array instead | the contract text, harvest's validator and the CLI all implement the object shape, so moving the contract would break two working surfaces to accommodate one broken one
  • r-mcpdraft291 d4444d5 — Accepting only the object shape and dropping bare-array support | the earlier tool description advertised an array, so a caller may exist that sends one; refusing it now would trade this defect for a different one

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.

@MongLong0214
MongLong0214 merged commit 1e79e3b into main Aug 1, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant