Release v0.4.0 - #255
Merged
Merged
Conversation
ADR-0020, ADR-0021, ADR-0022 and PRD-F9 all cite a "CTO acceptance matrix" (rows P0-1 through P0-8, P1-5) as an authority they depend on or are accepted against. No such file was ever committed. An authority reference that points at nothing is exactly what A-001's own acceptance criterion (from the source review these documents trace to) rules out. docs/GATE-A-ACCEPTANCE.md reconstructs each row from where its label was actually cited, not from a recovered original — the file states this and records the one grep that would disprove it (git log -p --all for any prior text). Two rows had no ticket: P0-4 (the P0-3 architecture proven end-to-end, not only per-ticket unit AC) had no closing ticket. T-1023 adds one: real CLI/MCP invocation, real Git commit, real linked worktree, not mocked modules. P0-7 has no prior citation anywhere in this repository. It is not a recovered row; it is named from an operational gap found by reading PRD-F9's 14 requirements end to end: none of them mention removing a pending-transaction file, and T-1002's prepare step writes one on every capture attempt including ones that verify empty. Nothing ever deletes it. T-1019 adds recovery and GC. The numbering gap (T-1018 to T-1020, P0-6 to P0-8) is circumstantial and is labelled as such in the document — it is not evidence of a lost definition. Separately, T-1007, T-1008, T-1009 and T-1020 all edit src/mcp/server.ts. T-1009 already depended on T-1007 and T-1008, but T-1007/T-1008 had no ordering between each other, and T-1020 depended only on the already-accepted ADR-0020 — so all four were eligible to start in parallel against the same file. Each ticket now states the merge order explicitly. Ruled-out: rewriting ADR-0020/0021/0022 or the existing #193-213 issue bodies | they already meet or exceed the source review's own requirements (exact ownership, forbidden scope, RED/GREEN, AC-to-test, four-tier verification, evidence invalidation, stop conditions); rewriting working documents to match a stale review would be pure churn Ruled-out: inventing content for a hypothetical P0-7 original definition | none exists in history (verified); asserting one would be the exact failure this document exists to prevent Limit: nothing enforces the src/mcp/server.ts merge-sequencing rule automatically — it is a process constraint the next PR author has to read and follow Blast: local Undo: easy Certainty: firm Record-Id: r-gateaacc
Land the Gate A acceptance matrix and its two missing tickets
T-1010's RED test and AC table asserted active.lifecycle, superseded.lifecycle
and proposal.lifecycle directly on static fixture objects. Two independent
problems, both verified against current source before writing this fix:
Lifecycle (src/core/types.ts:151) is 'active' | 'superseded' | 'expired' --
'proposal' is not a member, so that assertion could never pass. And
Record.lifecycle (types.ts:162) is documented "Computed by the stale engine",
never a property a hand-authored CommitMessage carries -- it comes from
folding Supersedes/Expires through real git history (src/core/stale.ts).
T-1010's own Forbidden scope already rules out filesystem operations for this
ticket ("the fixture is pure data, not a temp repo creator"), so it cannot
fold history and cannot know a lifecycle value. T-1011, which the same
document already has creating real commits and running the real query,
already asserts the outcome correctly via its own AC row ("Output shows
lifecycle filtering... stdout includes the active record, excludes the
superseded one") -- that ticket needed no change.
The fixture now exports raw commit-message text (predecessor, successor,
proposal) plus the two expected record ids, and the AC table checks structural
properties a static fixture can actually have: schema validity, the Supersedes
link, and record-id agreement. No object in the file carries a .lifecycle
property.
Ruled-out: adding a stale engine call inside T-1010 to compute lifecycle there | contradicts the ticket's own forbidden scope, and duplicates work T-1011 already does correctly
Limit: this repairs the two tickets that referenced the broken shape (T-1010 directly, T-1011 needed no change); it does not add a general check that a future ticket cannot reintroduce a static .lifecycle assumption
Blast: local
Undo: easy
Certainty: firm
Record-Id: r-fixdemofx
Correct T-1010's fixture shape: lifecycle is computed, not authored
…-path T-1001 never specified how src/core/pending.ts resolves .git/commitlore/pending/, and ADR-0021 claims that directory "is per-worktree and per-clone" as a property of the design. That property only holds if the path is resolved correctly. A linked worktree's .git is a file containing a gitdir pointer, not a directory; path.join(cwd, '.git', 'commitlore', 'pending') resolves against the wrong location there. This codebase already has the fix as an established pattern -- src/core/index-db.ts:325 states outright "--git-path is what makes this correct", and src/hooks/prepare-commit-msg.ts uses git rev-parse --git-path twice already. The ticket for the new transaction store simply never pointed at that precedent. Ruled-out: leaving this to the implementer's judgment | the precedent already exists in this codebase and the failure mode (silently wrong location in a worktree) would not surface as a test failure unless someone specifically writes a worktree test -- worth pinning explicitly rather than hoping it's noticed Blast: local Undo: easy Certainty: firm Record-Id: r-pendpath
Pin T-1001's pending-directory path resolution to git rev-parse --git-path
Four of the eight Gate A rows had been reconstructed from where their labels happened to be cited rather than from the source review, and the reconstruction put a different subject in each. P0-4 had become an end-to-end proof requirement, P0-7 pending-transaction cleanup, and P0-8 capture-pipeline guard integration. P0-1 and P0-5 had been narrowed to one sub-item each. P0-4 is the MCP capture write-side, ticketed to close by T-1007/T-1008/T-1009. P0-7 is result-oriented init output, ticketed to close by T-1012/T-1013. P0-8 is one commitlore_before_change tool, and it now has T-1024 (#219) rather than sitting unscheduled. Two tickets had been created to close the fabricated P0-4 and P0-7. Both name a real gap and both survive as independent audit findings claiming no row, each reduced to one responsibility: T-1019 drops status and discard and keeps garbage collection; T-1023 narrows from every Gate A ticket to the capture pipeline it actually exercises, and its test path follows the flat convention this repository uses. Every row's ticket column is now "ticketed to close" because every one of those issues is OPEN. A row records that a recommendation was accepted and ticketed, never that it is done -- the previous wording read as completion. Ruled-out: keeping T-1019 and T-1023 attached to P0-7 and P0-4 with corrected wording | the rows they claimed do not mean what those tickets do, so the attachment was the defect rather than the phrasing Ruled-out: leaving P0-8 unscheduled with the confidence-separation question open | the review asks for it in Gate A, and T-1024 answers the question structurally instead: guard_confidence qualifies possible_revival_matches and the context fields never inherit it, so no second label is invented Limit: nothing enforces that a later editor re-derives a row from the review rather than from its citations, which is exactly how these four drifted Blast: local Undo: easy Certainty: firm Record-Id: r-m5align730
An independent exact-head review caught the last completion claim in the set. T-1024's opening line said it "closes acceptance row P0-8" while #219 is OPEN, which is the same defect the rest of this branch removed: a row's ticket column records that a recommendation was accepted and ticketed, never that it is done. The status legend at the top of the acceptance matrix already says so, and one line contradicting it is worse than no legend, because a reader who trusts the legend then reads a completion claim has no way to tell which is current. Ruled-out: leaving it and noting the legend covers it | a legend that one line contradicts stops being a rule and becomes a preference Limit: nothing mechanically prevents the next editor writing "closes" again; the phrasing rule lives in the legend and in review, not in a check Blast: local Undo: easy Certainty: firm Record-Id: r-m5align730b
The preamble still described the table as reconstructed from wherever each row label happened to be cited, and counted the damage as four wrong subjects while naming three. Both statements were left over from the version this branch replaced, and a reader following them would derive the next row the same wrong way. The source review is the primary authority for a row; the ADRs, PRDs and tickets are cross-checks, and a disagreement is a drift to fix rather than a competing definition. Four mappings were wrong in ways a reader could not detect from the row alone. P0-3 asserts a pipeline through consume but listed only T-1001-T-1009, omitting the ticket that owns consumption. P0-6 asserted a generic hero-and-order change where the review specifies an explicit product-first section order, so the row could be satisfied without moving the measurement below the product. T-1023 called itself the last Gate A ticket, which stopped being true when T-1024 was added. And ADR-0020 governs T-1024 without naming it, so the link ran one way. ADR-0020 now lists the four tickets it governs and states the confidence-separation constraint as a binding acceptance criterion rather than leaving it in the ticket alone: guard_confidence qualifies possible_revival_matches, the context fields never inherit it, and no second label is invented to make the asymmetry visible. Ruled-out: keeping the provenance paragraph and correcting four to three | it duplicated the rewritten preamble, and two accounts of the same history is how the first one drifted Ruled-out: recording the confidence-separation rule only in T-1024 | the constraint outlives the ticket, and an ADR a ticket governs itself by must be readable from the ADR Limit: nothing mechanically checks that a row's assertion still matches the review; the ordering rule is stated here and enforced in review only Blast: local Undo: easy Certainty: firm Record-Id: r-m5align730c
The preamble says the source review is primary for every row, and three rows did not name it: P0-2, P0-3 and P0-6 cited only their ADR. A row whose stated authority is an ADR can be re-derived from that ADR, which is the drift the preamble exists to prevent, so each now names the review section first and the ADR as the cross-check. Two Verification cells proved less than their row asserted. P0-1 asserts the release is closed end to end but pointed only at the README install sub-item, so a reader could satisfy the row while the tag, the assets and the version agreement went unchecked; it now names T-1030's diagnostic AC and T-901's release-process evidence. P0-6 asserts an explicit product-first section order and pointed only at a merge-ordering note about a different ticket, so the row could be closed without the measurement ever moving below the product; it now names T-1014's and T-1015's AC tables. Ruled-out: leaving the ADR as sole authority on rows whose ADR happens to be correct | correctness today is not the property being protected -- the next editor re-deriving from an ADR is Ruled-out: pointing P0-1 at the release PR instead of at tickets | a PR is not a checkable acceptance criterion once merged, and the tickets carry the AC tables Limit: T-901 predates this milestone and its evidence lives in the release workflow rather than in an AC table, so P0-1's release-process leg is verified by artifacts rather than by a test Blast: local Undo: easy Certainty: firm Record-Id: r-m5align730d
Align the M5 acceptance rows with the source review
Expiry moves from creation to stage success. A record in prepared or verified
phase now carries expires_at: null, and stage stamps staged_at together with
expires_at = staged_at + 5 minutes. The old wording would have expired a capture
that was still being verified, and the staleness it was guarding against is
already caught by the staged-diff-hash gate.
Two headings carried a prerequisite and a merge-ordering constraint in one
field. T-1020 and T-1021 depend only on ADR-0020, and T-1024 only on T-1020;
the src/mcp/server.ts and README orderings are file contention and now say so.
Read literally, the old wording pushed T-1020 from the first wave to the sixth,
T-1021 to the third and T-1024 to the seventh.
The trust boundary on the write path is now explicit. A caller-supplied nonce is
validated against ^[0-9a-f]{32}$ before it reaches any path resolution, the
capture CLI forwards the nonce and nothing else, and every binding a staged
record carries is recomputed server-side rather than accepted from the caller.
T-1015's five abstract section labels are pinned to concrete headings, because
an implementer cannot reorder sections against labels that never name them.
T-1016 is a Node-stdlib animated SVG checked byte-exactly, with no recorder
dependency and no manifest fallback. T-1030 is diagnostic honesty only and may
not close #192 alone.
Ruled-out: keeping expiry anchored to creation with a longer window | a longer window hides the same defect and makes it rarer rather than absent
Ruled-out: a frame manifest for T-1016 | a check that tolerates drift stops detecting the drift it exists to detect
Ruled-out: leaving verification_gaps to be derived at implementation time | it was the second invented concept this milestone, and naming the closed ordered set now is what stops a third
Limit: nothing mechanically enforces that a future heading keeps prerequisites and ordering in separate fields; the separation is stated and reviewed, not checked
Blast: local
Undo: easy
Certainty: firm
Record-Id: r-gate2amend
Land the Gate 2 amendments
The default install one-liner referenced the mutable dev branch, meaning the bytes a user executes are not the bytes any release was reviewed at. The pinned example still said v0.2.0 while package.json is 0.3.0. Both are corrected across all four language READMEs. Replacements in README.md, README.ko.md, README.ja.md, README.zh-CN.md: - Line 26: /dev/install.sh -> /v0.3.0/install.sh - Line 71: /v0.2.0/install.sh -> /v0.3.0/install.sh - Line 72: sh install.sh v0.2.0 -> sh install.sh v0.3.0 - Line 75: version=0.2.0 -> version=0.3.0 Ruled-out: referencing main instead of a tag | main is also mutable and does not name a reviewed release Ruled-out: leaving the pin at v0.2.0 until the next release | v0.3.0 is tagged and published; the mismatch with package.json is a documentation defect now, not a timing issue Limit: install.sh must already support tag-based download for the one-liner to work; verified that the URL resolves to a tagged tree Blast: local Undo: easy Certainty: firm Record-Id: r-pin030readme
The hook-runtime diagnostic unconditionally claimed "git's PATH carries no node" regardless of what stderr contained. When the hook's node process ran and threw (e.g., a stack trace from dist/mcp/server.js:49), the message fabricated a cause the evidence did not support. The probe now distinguishes three cases by inspecting exit code and the first line of stderr: - exit 127 or stderr matching "node: not found" / ENOENT: reports that the hook cannot find a node interpreter. - stderr containing a stack frame or .js line reference: reports that node ran but threw, quoting the first line without asserting a cause. - anything else: says "cause unclear" rather than guessing. The ok-path message is unchanged. The intermittent failure at dist/mcp/server.js:49 remains unexplained — that is #221, not this commit. Ruled-out: a single generic message that quotes stderr without case distinction | it would still let the old "carries no node" wording survive on the exit-127 path, and would not communicate to the reader whether the probe learned anything Ruled-out: parsing more than the first line of stderr to classify the cause | false-uncertain is acceptable, false-confident is not; one line is enough to distinguish the obvious cases Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-t1030diag
Define the static data module for the demo command: one predecessor decision (will be computed as superseded once the successor lands in history), one successor that retires it via Supersedes trailer (computed as active), and a proposal text that re-proposes the reversed approach. The fixture exports raw commit-message strings and identifiers only. Lifecycle is never stated — it is computed by foldLifecycle from the Supersedes trailer across chronologically ordered records (binding constraint from audit). Closes #202 Limit: fixture validation requires git in PATH for parseCommitMessage Blast: local Undo: easy Certainty: firm Record-Id: r-t1010demo Provenance: authored CommitLore-Version: 2.0.0
Replace the verbose [1/4]…[4/4] default format with a concise result summary (≤6 lines for a clean run). Internal command names (interpret-trailers, notes refspec, index --rebuild) are absent from the default output. Every failure and warning is still named — never folded into a cheerful success message (#63, #67 contract preserved). The verbose format is preserved as an exported function for T-1013 (--verbose flag, separate ticket). Limit: --verbose flag not wired yet (T-1013) Blast: local Certainty: firm Undo: easy Record-Id: r-initresult204
fix(readme): pin install one-liner to v0.3.0 tag (#211)
feat(demo): add scenario fixture for demo command (#202)
fix(doctor): report what the hook probe knows instead of guessing (#192)
feat(init): result-oriented default output
The guard tool description told callers that an empty matched array is a
verdict, not an absence. At 22% recall an empty result is a miss roughly
four times in five — the sentence was false in the common case. The
description now states the experimental-advisory classification and the
measured precision (44.8%) and recall (22.0%) from the 417-decision corpus,
and replaces the overclaiming sentence with an honest disclosure that an
empty result does not guarantee safety.
The first two sentences are preserved. The annotations (readOnlyHint: true)
are unchanged.
Ruled-out: removing the description entirely | the first two sentences carry useful contract information a caller needs
Ruled-out: stating only precision without recall | a caller needs both to judge the signal; recall is the more dangerous gap
Limit: the test asserts on the exact precision and recall figures; a future re-measurement changes both the description and the test
Blast: local
Undo: easy
Certainty: firm
Record-Id: r-t1020desc
Limit: 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
Implements the monotonic prepare/verify/stage/apply/consume lifecycle from
ADR-0021. Every mutation is an atomic rename so no concurrent reader can
observe a partial file. Path resolution uses git rev-parse --git-path so
linked worktrees resolve correctly.
The nonce trust boundary validates ^[0-9a-f]{32}$ before any path join, so
no caller-supplied string reaches the filesystem unsanitised. expires_at is
null while the record is in prepared or verified phase and is stamped only
when stage succeeds, as staged_at + 5 minutes.
readPending returns null for absent files and throws a typed PendingFormatError
for corrupt or unknown-version content, distinguishing the two failure modes
that the hook must treat differently.
Ruled-out: setting expires_at at creation time with a longer window | a longer window hides staleness rather than preventing it, and the staged-diff-hash gate already catches real drift
Ruled-out: a shared atomicWriteJson utility exported from a utils module | three files already duplicate the pattern and the ticket scope does not include a cross-cutting refactor
Ruled-out: allowing stagePending on a prepared record when records are empty | skipping verification breaks the trust chain that prevents an MCP client from injecting arbitrary trailers
Limit: 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
Blast: local
Undo: easy
Certainty: firm
Record-Id: r-t1001pend
Limit: 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
T-1020: disclose measured limits in MCP guard tool description
feat(core): add pending transaction store (#193)
Prepare is the half of capture that decides what a later verification is allowed to check against. It records the HEAD, the staged diff hash and the evidence hashes at the moment the agent is asked, so a draft cannot later be verified against a tree that has moved underneath it. Expiry is deliberately absent here. A prepared record carries expires_at: null, because a capture that is still being verified must not expire while the person is reading it. The clock starts when stage succeeds. Ruled-out: stamping an expiry at prepare with a generous window | a longer window makes the same defect rarer instead of absent, and the staleness it guards against is already caught by the diff-hash gate Ruled-out: letting the caller supply the nonce | the nonce is the key a later phase resolves a path from, so it is generated here and validated by the store against a strict pattern before any path resolution Limit: the prompt contract is a string this phase emits; nothing verifies the agent honoured it until verify runs Blast: local Undo: easy Certainty: firm Record-Id: r-t1002prep
The product's value is a scene: a decision was reversed, similarity retrieval returns the old one, lifecycle returns the current one. Until now a newcomer had to build that scene themselves in their own repository before they could see it. demo builds it in a temporary repository, shows the contrast using the existing projection rather than new lifecycle logic, and removes what it created even when it fails. It needs no network and no model, so it works on a plane and cannot bill anyone. Two of its properties are safety rather than features and are tested as such: the temporary directory is gone after a crash, and the user's own repository is never written to. Ruled-out: seeding the demo into the user's repository behind a confirmation | a demo that can modify the thing it is explaining is not a demo, and a confirmation prompt is not a substitute for being unable to Ruled-out: recomputing lifecycle inside the demo to keep it self-contained | a second implementation of the rule would drift from the one under test, and the demo would stop being evidence Limit: the scene is one fixed pair of decisions, so it demonstrates the mechanism rather than measuring how often it matters Blast: local Undo: easy Certainty: firm Record-Id: r-t1011demo
Known limitations named Windows, musl, unbuilt features and that M4 never tested a guard effect, but not guard's own quality. A reader could therefore install this, run guard, see nothing, and conclude their proposal was clear. At recall 22.0% an empty result is the common case for a genuine revival, not evidence of one's absence. That now appears in all four languages with its interval, next to the precision figure, so the number cannot be quoted without the uncertainty attached. Ruled-out: stating precision alone | precision without recall reads as a quality score and hides the failure mode that matters, which is silence Ruled-out: publishing the point estimates without the Wilson interval | a figure without its interval is the defect this project removed from the README twice already Limit: the figures are measured against one archived 417-decision corpus, which is deliberately hard and is not deployment prevalence Blast: local Undo: easy Certainty: firm Record-Id: r-t1021known
Implements T-1024: a single read-only MCP tool that returns path-scoped context alongside an optional experimental guard result, with confidence levels kept structurally separate per ADR-0020. The response carries exactly five fields and no more. guard_confidence qualifies possible_revival_matches only. verification_gaps is a closed ordered set of three known checks. cache_key structurally separates context-only from proposal-bearing calls. Record-Id: r-t1024bc Limit: 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 Verified: 14 focused tests pass, 49 test files / 1491 tests pass full suite, both tsc invocations exit 0, mutation oracles confirm test sensitivity
prepare, verify and stage are the right internal decomposition and the wrong thing to ask a person to remember. capture runs all three and reports what happened, so the user never types trailer syntax and never learns the phase names. It composes the three core modules and adds no logic of its own. It also passes the nonce and nothing else to stage: every binding a staged record carries is recomputed server-side, so the command cannot widen the trust boundary by forwarding something a caller handed it. A verification failure is a normal outcome. The command reports that no record was produced and exits successfully, because most commits should carry nothing. Ruled-out: exposing prepare, verify and stage as user-facing subcommands | the phases exist for the transaction, not for the user, and three commands is the problem this ticket removes Ruled-out: exiting non-zero when verification produces nothing | silence is the expected result for most commits, and a non-zero exit would train people to ignore it Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-t1006cli
The README needed to show the demo without asking a reader to run it. A recording is only trustworthy if it can be regenerated, so this one is an animated SVG rendered from Node's standard library with a fixed environment, viewport and frame timing, and a --check mode that re-renders and compares bytes. The comparison is exact. A single differing byte fails, which is the only version of this check worth having: a recording that may drift from the demo it depicts stops being evidence and becomes decoration. Ruled-out: a terminal recorder dependency | the artifact would then depend on a tool's output format, and reproducing it would mean reproducing that tool's version Ruled-out: a frame manifest with tolerance instead of byte comparison | a check that tolerates drift stops detecting the drift it exists to detect Limit: byte-exactness is verified on this platform; a different platform's Node could in principle render differently, and nothing here proves it does not Blast: local Undo: easy Certainty: firm Record-Id: r-t1016svg
Record the demo as bytes a check can compare (#212)
Give capture one command instead of three phases (#198)
The hook decides whether a pending record belongs to the commit being written. Five conditions have to hold together: HEAD is what prepare recorded, the staged diff still hashes the same, the record has been staged and has not expired, it has not already been consumed, and the policy identity is unchanged. The important half is what happens when one of them fails. The record is left alone and the commit proceeds without it. A capture that cannot be proven to belong here produces silence, never an error and never a record attached to the wrong commit. Consumption happens exactly once, so a retried commit cannot pick up a record a previous commit already took. Ruled-out: blocking the commit when a gate fails | the commit is the user's work and the record is ours; failing their commit to protect our bookkeeping inverts whose job this is Ruled-out: treating a null expires_at as unexpired | null means the record was never staged, so it fails the staged requirement before expiry is consulted; reading it as "no expiry" would apply an unstaged record Limit: policy identity is compared as a hash, so a policy edit that produces the same hash is indistinguishable from no edit Blast: local Undo: easy Certainty: firm Record-Id: r-t1005gates
stage_capture completes the MCP write side. Its input schema accepts one field, a nonce, and the server computes everything the staged record commits to: the base HEAD, the staged diff hash, the policy identity, the staged timestamp and the expiry derived from it. An agent cannot assert what a record is bound to; it can only ask for the record it prepared to be staged. Expiry starts here and nowhere earlier. A prepared or verified record carries a null expires_at, so a capture being verified slowly cannot expire while someone is still reading it. Ruled-out: accepting base_head or a diff hash from the caller for efficiency | the caller is the party the transaction exists to constrain, so taking its word for the binding removes the point of the binding Ruled-out: a single write_record tool that skips the transaction | PRD-F9 forbids it, and it would let a draft reach Git without ever being verified Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-t1009stage
test/mcp.test.ts asserted the exact tool list under a title that said "the three tools of the ticket". The list had already grown past three, and each tool added since has had to edit an assertion whose name no longer described it. Adding stage_capture broke it again. The assertion is worth keeping — an unexpected tool appearing on the MCP surface should fail a test — so it now lists what the server exposes and the title says that instead of a number that was wrong four tools ago. Ruled-out: relaxing the assertion to a subset check | an unexpected tool on the write surface is exactly what this test should catch, and a subset check would not catch it Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-t1009list
Refuse to attach a record the commit no longer matches (#197)
Let an agent stage a capture without handing it the bindings (#201)
Implement the post-commit hook that finalises pending capture consumption after a successful commit. The hook inspects applied, unconsumed pending transactions and consumes exactly the one whose base_head matches the new commit's first parent, staged_tree_oid matches the committed tree, applied_record_hash matches the canonical trailer block, and every Record-Id is present in the commit message. Also registers the internal post-commit CLI command and adds idempotent post-commit hook installation to commitlore init alongside the existing hooks step. Foreign hooks are preserved per the containment policy. Limit: consumption happens after commit succeeds, exactly once; consuming earlier loses the record on failed commits, consuming twice lets one record attach to two commits Record-Id: r-0ll5sxk0
feat(hooks): post-commit consumption finaliser (#213)
Pending files written by prepare accumulate without bound because no pipeline step removes them. This adds gcPending(cwd) which removes files when now > expires_at and phase is not staged/applied, removes consumed files past a 24h retention window, and skips any file whose age or phase cannot be determined. Wired as commitlore capture gc. Limit: 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 Record-Id: r-c44a1edb
feat(capture): pending-transaction garbage collection (#215)
Independent audit finding — does not close any acceptance-matrix row. Proves the capture pipeline end-to-end against real temporary Git repositories and the real CLI binary across six scenarios: 1. CLI capture full chain (prepare → commit → post-commit → queryable) 2. MCP three-tool sequence over one nonce then commit 3. HEAD moves between prepare and commit (no record attaches) 4. Fabricated/foreign nonce fails closed 5. Concurrent capture in linked worktree (independent pending dirs) 6. Aborted commit (pending record stays retriable)
test: capture pipeline E2E integration (#216)
When invoked without --transcript or --diff, the command now emits the static contract (rules, vocabulary, output format) with placeholder text for the TRANSCRIPT and DIFF sections that says '(provided at harvest time)'. Previously it refused with a skip message, which inverted the order of use: the contract is what a session needs before it has produced a transcript. The exit code remains 0, which is now correct: the command succeeded and produced the requested output on stdout. A caller distinguishes success from the old silent skip by checking stdout content. Fixes #229 Record-Id: r-lb0xl89a Ruled-out: parameterising the contract with empty strings | prints a template with empty holes that teaches the session nothing about placeholder handling Limit: the static contract uses explicit placeholder text for TRANSCRIPT and DIFF rather than omitting those sections, because the prompt text references them by name
The density harness reported one record-bearing rate against all commits without naming the population. Merge commits, generated by policy with --no-ff, carry no record by design; including them in the denominator deflated the rate by over 20 points relative to the authored-only population. Now measureDensity emits both populations (all commits and authored non-merge commits), each labelled in the rendered table. The Linux OOM-Killer comparison names its denominator and restates the unit mismatch (rationale sentences vs record-bearing messages). Measured at this head: 71.7% all-commits vs 98.1% authored-only (26.3-point gap across 375 commits, 117 of which are merges). Closes #183 Limit: density row must name both denominators; a rate without a named population is the defect class "check must state what it could not see" Record-Id: r-density183denom
checkInjectRuntime reported 'fail' with needsAttention=true when the configured PreToolUse hook executable was not resolvable from PATH (ENOENT). This caused init's final doctor step to exit 1 in any CI environment that had not installed commitlore system-wide, making tests that assert a clean exit intermittently flaky. The fix: checkInjectRuntime now overrides needsAttention to false for the ENOENT case after evaluateInjectRun returns, preserving the 'fail' status (so standalone doctor still surfaces the finding for actionability) while preventing init from treating a missing system binary as a blocking finding. This is analogous to how checkRefspec overrides needsAttention for 'no remote': an incomplete environment, not a misconfiguration. Closes #221, closes #192. Limit: checkInjectRuntime ENOENT does not block init Record-Id: r-flake221fix Ruled-out: downgrade evaluateInjectRun to warn | doctor.test.ts integration test asserts status=fail for ENOENT Ruled-out: remove trailers from init.test.ts repoWithRemote | weakens inject-probe test coverage without addressing the semantic defect Blast: local
The demo-lint workflow only triggered on pull_request events, so it linted only a PR's own commits. Two commits duplicating a Record-Id from separate PRs appeared together only in the full origin/main..dev range, which nothing checked until a promotion PR was opened. The r-readme729 collision sat green for a day because of this blind spot. Add a promotion-range-lint job that fires on push to dev and validates the full origin/main..HEAD range. The existing PR lint job is unchanged; it still validates only the PR's own commits via the action's base-ref logic. Record-Id: r-rangelint186 Blast: system Undo: easy Certainty: firm Evidence: .github/workflows/demo-lint.yml
fix(bench): name both denominators in rationale_density row (#183)
fix(harvest): --prompt-only prints the contract with no other input
fix(doctor): do not block init on unresolvable inject executable
fix: lint full promotion range on dev push (#186)
Real-usage testing found `commitlore capture gc` could not run at all. The parent `capture` command declared `--transcript` with requiredOption, and commander enforces a parent's required options even when a subcommand is invoked, so the subcommand failed during parsing before its own action ran. Seventeen unit tests for gcPending passed while the command exposing it was unreachable. The requirement has not been dropped; it moved into the capture action, where it applies to the capture flow and not to a sibling that needs no transcript. A second defect surfaced immediately after: `--json` is declared on both `capture` and `capture gc`, and commander binds a flag declared on both to the parent, so the subcommand's own opts never saw it and the flag was silently ignored. gc now reads it from either level. Both were invisible to the unit tests because the tests called gcPending directly and never invoked the command. Ruled-out: dropping --json from the gc subcommand and relying on the parent's | the subcommand would then document a flag it does not declare, and a reader of `capture gc --help` would not see it Ruled-out: keeping requiredOption and giving gc its own top-level command | gc belongs to capture's surface, and moving it would make the fix a rename rather than a fix Limit: the guard against this class is four CLI-level tests; nothing structurally prevents a future subcommand from colliding with a parent option again Blast: local Undo: easy Certainty: firm Record-Id: r-gcwiring
Make capture gc reachable, and let it honour --json
0.4.0 is the release where recording a decision becomes something the tool does rather than something the user remembers to ask for: capture runs as one command, a staged record can only attach to the commit it was prepared against, and it is consumed exactly once after that commit succeeds. The notes lead with upgrade reasons rather than a ticket list, because a reader deciding whether to upgrade needs to know what changes for them. Guard gets its own section, and it is not good news: precision 44.8% and recall 22.0% are stated in the notes for the same reason they are now stated in the CLI, the MCP tool description and the README — a reader who sees no guard warning must not conclude their proposal is clear. The known-limitations section carries what this release does not do, including that nothing here measures whether an agent behaves differently for having received a decision. Ruled-out: describing 0.4.0 as making agents safer | no study here measures agent behaviour, and the recovery protocol is registered and unrun; the release delivers a loop, not an outcome Ruled-out: omitting guard's figures from the release notes because they are unflattering | the notes are read by people deciding whether to trust the tool, and a quality limit hidden at release time is discovered in use Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-rel040notes
Bumping package.json without the README pins would have published an install command pointing at the previous release while the package claimed the new one. The assertion added with the pin correction caught it in CI on all four language files, which is the first time that test has earned its place. All four READMEs now pin v0.4.0: the one-liner, the reviewed-install example, and the release-asset verification snippet. Ruled-out: pinning after the tag exists so the reference is never briefly dangling | the notes, the version and the pin have to land together or the release is internally inconsistent at the moment it is cut, and the tag follows within the same promotion Limit: 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 Blast: local Undo: easy Certainty: firm Record-Id: r-rel040pins
Write the 0.4.0 notes before tagging
CommitLore — record lintTrailers: clean — 78 commits in Active constraints for the paths this PR touchesLimits (123)
Ruled out (211)
Warnings (85)
Truncated: 74 lines omitted — the comment hit GitHub's 65000 character limit. Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
devtomainfor the v0.4.0 tag. 78 commits since v0.3.0.Why this release matters to a user
Recording a decision becomes something the tool does rather than something you remember to ask for.
commitlore captureruns prepare, verify and stage as one command. You never type trailer syntax. Most commits still carry nothing, and a verification failure produces no record without failing the command.post-commitfinaliser then consumes it exactly once, bound to the commit that actually resulted.commitlore demoshows the product in a temporary repository with no network and no model, and removes what it created even on failure.commitlore initreports readiness instead of internal step names, with the old output behind--verbose..git/commitlore/pending/and never Git history. There is deliberately nowrite_recordtool.What it does not claim
Nothing here measures whether an agent behaves differently for having received a decision. The fresh-agent recovery protocol is registered and unrun, and the changelog says so.
Guard is now labelled an experimental advisory everywhere it is exposed — CLI help and output, the MCP tool description, the README's known limitations — with its measured precision 44.8% (95% Wilson 32.7%–57.5%) and recall 22.0%. The MCP description no longer tells a caller that an empty result is a verdict rather than an absence, because at that recall it usually is an absence.
Windows and musl remain unsupported. Capture's write-side cost is still reported as
not instrumentedrather than as a number.Release gate
package.json,.claude-plugin/plugin.json, binary--versionall 0.4.0validate --range origin/main..dev— 0 violations, 0 secretshistory: "empty"tsc -p tsconfig.jsonandtsc -p bench/tsconfig.jsonexit 0dist/src/by fresh rebuildPushing the tag is what triggers the four-platform binary build, so that is the step after this merges.