Release 0.8.1 - #575
Merged
Merged
Conversation
0.8.0 shipped a protocol whose server described half of itself. The read half was there -- read the context resource before editing, how to read the trust tiers -- and nothing said when to record anything. Four of the seven hosts `install.sh` wires receive an `mcpServers` entry and no skills, so they held the capture tools with nothing telling them what the tools were for, and `AGENTS.md` was the only channel carrying the missing half. That is why `init` wrote the file into repositories whether or not they use the convention. The server carries both halves now, and writing AGENTS.md is `--agents-md`, off by default. Also: a registration naming a path that does not exist is reported instead of counted as healthy. Four hosts on the author's machine pointed at a temp directory deleted long ago, and every reinstall said they were fine. Limit: the capture half reaches a host that surfaces MCP `instructions`; one that ignores that field still needs `--agents-md`, and nothing detects which kind a host is Blast: system Undo: easy Certainty: firm Verified: version consistent across all three manifests, both lockfile fields and the built CLI at v0.8.1, with thirty pins updated across four READMEs and both installers; 199 pass across readme, manifest, check-release-version, init, mcp and both installer suites Provenance: authored Record-Id: r-release081
CommitLore — record lintTrailers: clean — 26 commits in Active constraints for the paths this PR touchesLimits (268)
Ruled out (547)
Truncated: 629 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. |
`unverifiable` warned and carried on to `codex plugin add commitlore@commitlore` — which installs whatever that marketplace serves. A marketplace somebody else had configured under this name, on a Codex that does not report marketplace sources, could therefore supply the plugin while the install reported CommitLore. Refusing every unverifiable case would have been the wrong repair: it also breaks an older Codex where the marketplace is genuinely ours and only the source field is missing. Presence is what decides. A marketplace this install adds itself is ours by construction; one that already exists under our name and cannot be identified is the case worth refusing, and those are distinguishable even when the output is a table this cannot parse — the name is still in it. So `unverifiable` splits. Present and unidentifiable is refused with the same reasoning as `foreign`, and names the two ways out. Absent is noted and the install proceeds to add ours, because the question of whose it is does not arise. Limit: presence is read from the marketplace name, so a Codex that reports neither a source nor a listing this can parse is treated as absent and one is added under a name that may already be taken Blast: module Undo: easy Certainty: firm Verified: six states classify correctly against real `codex plugin marketplace list --json` output and hand-built table output — ours, foreign, absent, unverifiable-present, unverifiable-absent, and an entry whose source field is missing; a present-but-unidentifiable marketplace exits 2 having issued only the listing call and written no ownership marker; 71 pass across codex-plugin and both installer suites Provenance: authored Record-Id: r-codexunver1
`engines.node` said `>=22`; the index is `node:sqlite`, which does not exist before 22.5. The source comment in `core/index-db.ts` already said so — "which satisfies but predates the `>=22` floor in `package.json`" — and nothing acted on it. Both installers checked the major only, so 22.0 through 22.4 installed cleanly and then could not build an index. The lazy resolve means that is not a crash: the query falls back to a full scan, and the repository reads as slow rather than misconfigured. "No index here" and "your Node is too old" are different facts and only one of them tells the operator what to do. The floor is declared where it is true and enforced on both installers, with a message that names the version and why. Limit: this bounds the version, not the feature -- a Node that ships `node:sqlite` behind a flag, or removes it, is not detected here Blast: local Undo: easy Certainty: firm Verified: the shell installer refuses v22.4.0 naming node:sqlite and 22.5, accepts v22.5.0 and v24.1.0, and still refuses v21.9.0 on the major; install.ps1 gains the same two-part check; 38 pass in install-script and 23 in install-ps1 Provenance: authored Record-Id: r-nodefloor1
`doctor` writes paths home-relative, so this checkout appears as `~/…` under `$HOME` and as an absolute path anywhere else. The snapshot was recorded in the first case and asserted `~<path>`, so every checkout outside a home directory failed a test about text the report had produced correctly. The normaliser now collapses both spellings to `<root>`, which is what the snapshot was always trying to say. Limit: this normalises the two spellings this report produces; a third form -- a symlinked home, a UNC path -- would need its own Blast: local Undo: easy Certainty: firm Verified: the suite passes from a checkout under $HOME and from one under /private/tmp, which is the case that failed before Provenance: authored Record-Id: r-snapshome1
…it does Three ways this told a reader or a model something that was not so. **`commitlore_stale` served ungraded payloads.** `commitlore_query` grades every record and renders a blocked one as a count; the stale handler serialised `resolvedTrailers` straight into its report, and the MCP server returns that report verbatim. So an expired `Warn: IGNORE ALL PREVIOUS INSTRUCTIONS…` reached a model through a tool on the same server, ungraded — the payload only had to be stale, which is the one state nobody watches. Reproduced against the built CLI: the sentence appeared in `stale --json` in full. Stale records are still listed, because what is stale is the operator's business. Their values are withheld when a pattern matches, and their keys are kept, a key being a closed vocabulary that cannot carry prose. **The advertised one-liner was not pinned.** It fetched `v0.8.1/install.sh` and passed no version, and with no argument the script resolves the newest remote tag. Once 0.8.2 exists, that command installs 0.8.2 while `docs/install.md` calls it pinned to the current release. It now passes the version its own URL names. **The documentation still described the old AGENTS.md behaviour.** `init` stopped writing that file by default in this release, and the README, both the compatibility table and the prose, `docs/install.md`, `docs/capture.md`, `docs/cli.md` and all three translations still said it wrote the pre-edit instruction. A reader following that path got neither. They now say where the procedure actually lives — the MCP server's instructions, on every connection — and that `--agents-md` writes the file for a host that reads the convention instead. Limit: withholding uses the same pattern table as every other route, so a payload that trips nothing still passes; this closes a route that had no grading at all, not the heuristic behind it Blast: system Undo: easy Certainty: firm Verified: the payload appears in `stale --json` before the change and is withheld after, with the record still listed and its keys intact; removing the withholding fails the new case; 136 pass across stale, readme and mcp; no public surface still claims `init` writes AGENTS.md Provenance: authored Record-Id: r-staleclaim1
Two repairs from earlier today were written against one call site each, and the review found the sibling in both cases. **The dead-registration check missed opencode.** It has its own writer for a different config shape, so the generic path learned to name a target that no longer exists while opencode kept reporting one as fine — and the changelog named opencode among the four hosts the fix covered. The rule is now one shared function on both installers, because two copies of it is how the first one drifted. **The scan budget bounded only the commit pass.** `scanTrailers` always runs the notes pass afterwards, unbudgeted, in batches of 1024, parsing every note and resolving paths. A repository whose records live in notes could stall an edit well past the advertised ceiling while `unreadCommits` reported 0, because nothing counted what that pass had skipped. Both passes now read the same deadline, and the cost record has a half for each — they stop independently, so one number could not describe both. The budget regressions had no notes fixture, which is why this survived. They do now: forty notes, a clock already past the deadline, and an assertion that both halves report what they skipped. Limit: the budget bounds the two scans, not the command -- process startup, path resolution and rendering still sit outside it Blast: module Undo: easy Certainty: firm Verified: a deleted command in an opencode config is now named on both the shell and PowerShell installers, and a live one still reports as before; a budgeted scan over a repository with forty notes reports unread commits and unread notes separately, and reverting either fix fails its own case; 237 pass across query, index-db and both installer suites Provenance: authored Record-Id: r-secondcopy1
**Verification.** The earlier repair checked `persist`'s refusal on the accepted path. Four early exits — transcript mismatch, diff mismatch, unfetched notes, unavailable history — were each written as `persist(result); return result;` by hand and kept discarding it. Replaying a nonce through any of them returned a rejection to the caller while the first call's record stayed stored, and staging reads what is stored. Every exit now goes through one function that substitutes an unbound result when the store refuses. The exception handler is the single exception, and says why: it already returns `empty` with `incomplete: true`, so a failed store cannot change its answer. **Ownership.** A legacy wrapper was accepted for replacement whenever a directory named for its version existed under the data root. A directory called `v1.2.3` is something anyone can create, so the check asked about a name an attacker controls: an unrelated executable printing `1.2.3` was destroyed by creating one empty directory. Reproduced exactly that way. The evidence is now `dist/commitlore.mjs` inside that directory — written by this install and by nothing else. The regression fixture created an empty directory and passed without establishing anything, which is how the hole survived a test named for it; it now writes the runtime, and the name-only case has a test of its own. Limit: the runtime's presence proves this installer wrote the directory, not that its contents are unmodified since Blast: module Undo: easy Certainty: firm Verified: a replay through the transcript-mismatch exit returns empty and incomplete while the stored transaction keeps the first call's record, and reverting the helper fails two cases; an unrelated executable survives when only a directory of the right name exists and is still replaced when the runtime is there; 124 pass across capture-verify, capture, mcp and both installer suites Provenance: authored Record-Id: r-dropfake01
…we claim **`init` said ready over a repository where nothing can start a capture.** A failed MCP registration was reported at code 0, so every step got a checkmark and the run finished `init: ready`. Capture is the product; a setup command that could not wire it is not ready, whatever else worked. It is still not fatal — hooks, index and delivery all install — so this is 1, the code that already means "ran, and something needs you", never 2. And it now prints the repair: the exact `.mcp.json` to write, and `commitlore doctor` to confirm it. The operator is the only one who can decide what belongs in that file, so telling them beats guessing for them. **The compatibility matrix claimed executions CI did not perform.** It defines "supported" as an install path reaching the host *and the result being executed there*. macOS was marked supported while the row itself admitted the install ran on Linux only, and the musl row named two architectures the workflow never used. Rather than narrow the claims, CI now does what they say. `install-macos` runs `install.sh` on `macos-latest`, checks the wrapper reports the requested version, and then runs `init`, `doctor` and `context` in a repository it sets up. `install-alpine` does the same inside `alpine:3.21` on `linux/amd64` and `linux/arm64`, the second through QEMU. Prose cannot be tested, but a job name can: a new case reads every job the matrix cites as evidence and fails if CI does not define it. That is the cheapest check that would have caught both rows. Limit: the new jobs establish that an install runs and answers on those hosts, not that every command behaves identically there Blast: system Undo: easy Certainty: firm Verified: a repository whose .mcp.json cannot be registered now reports `2 step(s) need(s) attention — MCP registration`, exits 1, prints the file to write, and never prints `init: ready`; the workflow parses with the two new jobs present and passes the action lint; citing a job CI does not define fails the new case; 121 pass across init, compatibility-matrix, action-lint and doctor Provenance: authored Record-Id: r-readyhosts1
…cies need Yesterday's floor change said `>=22.5`, and `check-engines.mjs` read that range by scanning it for digits and taking the smallest — so it measured every dependency against **Node 5**. Both required jobs failed at that step, and typecheck, build, the whole suite, dogfooding, the performance gate and the fresh-clone check never ran behind it. The check that exists to keep `engines.node` honest is the thing that broke the release. It survived because the parser was inline in a script with no test of its own. It is now `scripts/engine-floor.mjs` with seventeen cases, including the exact shape that failed: `>=22.5` must not read as 5, and `>=22.12.0` must not admit 22.5.0 because their majors match. With the parser correct, the real mismatch surfaced: `commander` requires `>=22.12.0` while the package promised `>=22.5`. `node:sqlite` needs 22.5 and that dependency needs 22.12, so the floor is the higher of the two and every place that states it now says 22.12 — package, both installers, their messages and the version matrix in their tests. The `install-alpine` job added with this release also failed, on `dubious ownership`: it set `safe.directory` with `--global`, and the container runs as root with a different HOME than the runner that owns the checkout, so git never read the entry back. `--system` puts it where git looks. Limit: the parser covers the range shapes npm packages actually publish -- comparators like `>=22 <23`, and pre-release identifiers, are read by their first version and not by their bounds Ruled-out: adding semver as a dependency to parse this | one regex over a handful of published shapes does not justify a runtime dependency in a check that runs before install Blast: system Undo: easy Certainty: firm Verified: the engine check passes naming Node 22.12.0, seventeen parser cases pass including the two that encode this defect, both installers refuse 22.4 and 22.11 and accept 22.12, and version consistency holds across all three manifests, both lockfile fields and the built CLI Provenance: authored Record-Id: r-engfloor01
The container script is one single-quoted argument to `sh -c`, so a nested single-quoted `'*'` closed the quote and git received one argument where it wanted two -- `wrong number of arguments, should be 2`, exit 129, before any install ran. Double quotes inside the outer single quotes reach git intact. Limit: this is the second quoting layer in this step; a third -- a value with a double quote in it -- would need a different mechanism than more escaping Blast: local Undo: easy Certainty: firm Verified: the workflow parses and the step now carries both `safe.directory` arguments as separate words; the action lint passes Provenance: authored Record-Id: r-mslquote1
`validate` answers `reference: not-checked` when the notes mirror was never fetched, and exits 0 anyway — the reference half is skipped, not failed. CI ran it for its exit code and never fetched `refs/notes/commitlore`, so the gate that proves this repository keeps its own protocol had been passing with half the check unperformed (#542). Both halves matter because both are authoritative here: commit trailers and the notes mirror. Checking references against only one of them is not checking them. CI now fetches the mirror and reads the JSON: `shape` and `reference` must each report `ok`. `not-checked` fails, which is the whole point — a check that was skipped is not a check that passed. The fetch is `|| true`, because a fork without the ref should fail on the assertion that says references were not checked, not on a network error that says nothing about this repository. Limit: this asserts the two checks `validate` performs; a third class added later is not required by name until somebody adds it here Blast: local Undo: easy Certainty: firm Verified: against a real repository the assertion prints `shape: ok` and `reference: ok` and exits 0, and the same report with the reference status set to `not-checked` exits 1 naming the reason; the workflow parses and the action lint passes Provenance: authored Record-Id: r-dogfoodref1
…ous pass Every suite tested a part; nothing asserted the product's whole claim — that a decision recorded through the tools an agent calls survives into the commit and reaches whoever edits that path next. `test/capture-pipeline-e2e.test.ts` runs the real `dist/commitlore.mjs mcp` as a child process against a real repository with real hooks: prepare, verify, stage, commit, read back. The persistent client is why this one works. An earlier attempt drove the server with `spawnSync`, which writes every frame and closes stdin; the server read that EOF as the client hanging up and exited before answering, and a missing response is indistinguishable from a rejected draft. `test/mcp.test.ts` already had the right client, so it is now `test/mcp-client.ts` and both use it. Writing this caught the same class of error in the new suite. The fabricated-evidence case passed — and for the wrong reason: `verify` was never handed the diff `prepare` had hashed, so every call was refused as a source-mismatch and nothing about fabricated quotes was being tested. It now passes the staged diff, and asserts the *reason* is a quote nobody said. A negative case that cannot say why it is negative is not a negative case. Four cases: the decision reaching git and coming back, a fabricated quote leaving no record, `initialize` carrying both halves of the protocol, and a replay through an early exit reporting empty and incomplete rather than leaving the earlier record stageable. Limit: the model's judgement -- deciding a change is worth recording at all -- is the one step no fixture stands in for, and stays a manual pre-release matrix Blast: module Undo: easy Certainty: firm Verified: four cases pass against the built artifact; disabling the evidence-quote check in harvest-verify fails the fabricated case, so the negative is load-bearing; the extracted client leaves test/mcp.test.ts passing unchanged Provenance: authored Record-Id: r-e2epipe01
…host jobs Five findings from the pre-tag review, four of them in fixes this same release made. **The stale redaction had a second channel.** `resolvedTrailers` was withheld and `expiresAt` was not — it carries the `Expires:` value verbatim and is serialised beside them, so a payload in `Expires:` still reached a model through the same tool. Redacting one field of two is not redacting. **A refused store left the earlier record stageable.** `settle` changed what was returned and not what was stored, and `stage` reads what is stored. So a replay told the caller "empty" while the commit would have carried the first record — the exact invariant the previous fix claimed. The transaction is now discarded: two verifications of one nonce have disagreed, and there is no reading of that where either result should reach a commit. `prepare` is one call away. **A missing transaction reported a confident empty.** A stale or mistyped nonce answered `empty` with `incomplete: false` — "nothing survived", from a call that found nothing to check. It is `incomplete: true` now, which is what not knowing means. **Both new host CI jobs proved nothing.** They ran `init --unattended --no-agents-md`, and that flag stopped existing when this release flipped the default to `--agents-md`. Commander rejected it, `|| true` swallowed the rejection, and `doctor` and `context` then measured a repository `init` had never touched — while the jobs stayed green and the compatibility matrix cited them as evidence. They now require init's own summary line, because the exit code cannot tell "a step needs attention" from "could not run at all". **The installers took a mention for a registration.** A grep for `"commitlore"` matched the word anywhere in an agent config — a note, an unrelated value — and reported the host already wired while nothing was registered. Both installers now ask whether a server is registered under that key. Also: Node 22.12 is enforced but the lockfile, four READMEs, `docs/install.md` and the compatibility table still published 22+. A user on 22.11 met the stated prerequisite and was refused by the script. Limit: `registers_commitlore` reads the key, so a config that registers under a different key -- a host with its own naming -- still reads as unregistered and is wired again Blast: system Undo: easy Certainty: firm Verified: an injection in `Expires:` is withheld in `stale --json` where it appeared verbatim before; a replay leaves no pending transaction and `stageCaptureRecord` returns null; a missing nonce reports incomplete; `init --unattended` prints the summary line both jobs now require, and `--no-agents-md` is rejected as the review said; a config that merely mentions the word is wired while a real registration is preserved; 106 pass across stale, capture-verify, capture, readme, compatibility-matrix and both installer suites Provenance: authored Record-Id: r-pretag01
…erified
The last two findings of the pre-tag review, both the same mistake: taking the
cheapest observable fact for the one that matters.
**Ownership was a file's existence.** The previous repair replaced "a directory
named for this version" with "a runtime file inside it", which is still
something anyone can create — the regression fixture wrote a file containing
one comment and passed. Reproduced: an unrelated executable printing `1.2.3`
was destroyed by placing a comment at that path.
The evidence is now that the runtime *answers*: it runs and reports the version
the wrapper claims. Forging that means installing a working CommitLore of that
version, which is not an attack. `install.ps1` gets the same probe.
**Doctor called any launchable command a capture server.** `{"command":
"false"}` reported `ok` — a registration that starts nothing, described as
readiness. Preserving an operator's entry is right; vouching for it is not, and
those had been the same branch.
They are separate now. The command `init` writes reports `ok`. Anything else is
`warn`, names the command, says plainly that whether it starts a capture server
is unverified, and is still left exactly where it was. The test that codified
arbitrary wrappers as healthy now asserts both halves: preserved, and not
claimed.
Limit: `warn` distinguishes ours from not-ours by the command string, and does not execute anything -- a wrapper that really is a CommitLore server still reads as unverified, which is the safe direction but not a probe
Blast: module
Undo: easy
Certainty: firm
Verified: an inert file at the runtime path leaves a foreign executable intact while a runtime that reports the version is still replaced; doctor answers warn for `"false"` and for a wrapper path, ok for the command init writes, and rewrites neither file; 113 pass across doctor and both installer suites; engine check, dist reproducibility and version consistency all clean
Provenance: authored
Record-Id: r-answerown1
The entry described three fixes. Three independent reviews then found twenty-one blockers against this candidate, and most of the release is the answer to those — including several defects in the fixes made earlier in the same release. A changelog that named only the first three would be the same kind of claim this release spent its time removing. Grouped by what a reader needs: what reaches every host now, what `init` stops doing to their repository, what is served and what is claimed, what installing establishes, and what CI establishes that it did not before. Blast: local Undo: easy Certainty: firm Verified: readme, manifest and release-version suites pass against the rewritten entry Provenance: authored Record-Id: r-changelog81
A new reader learned that delivery and capture are different layers well down the page, after the install command they had already run. The split is the first thing that decides whether this tool does what they expect: delivery happens on its own, capture happens when an agent judges a change worth recording, and an ordinary `git commit` cannot start it because a hook has the diff and a capture needs the session. That now sits above the fold, in all four languages. The host table had gone stale in a way this release caused. It ended with one row for "other `AGENTS.md`-convention hosts", written when that file was the only channel carrying the capture procedure. It is not any more: Gemini, Cursor, Windsurf and opencode receive the MCP server from `install.sh` and get the procedure in its `instructions`, which is what a host that loads no skills has to work from. They are their own row now, named, with what they actually receive — and the caveat that whether a host surfaces those instructions is the host's choice and nothing here detects it. The Node badge still read `>=22` while the package, both installers and every prerequisite line say 22.12. Blast: local Undo: easy Certainty: firm Verified: readme, compatibility-matrix and manifest suites pass across all four READMEs; the badge, the prerequisite lines and package.json now state one floor Provenance: authored Record-Id: r-readme81
`validate` exits 1 when it finds violations, and under `set -e` that killed the step before the assertion could read the report — so the gate that was supposed to name which check failed reported only that something had. Nine minutes of output, and nothing said what was wrong. The exit code is not discarded; it is re-derived from the JSON, which is the thing that can name a violation. Violations are now listed with their commit and rule. I briefly added a baseline of three commits guessed from warning lines in the CI log, then removed it: validating one of them directly reported zero violations, so the guess was wrong. An exception list nobody verified is the kind of claim this release has spent its time removing. Blast: local Undo: easy Certainty: firm Verified: the workflow parses and the action lint passes; validating a single commit reports shape ok, reference ok and no violations, which is what showed the guessed baseline to be wrong Provenance: authored Record-Id: r-gatesays01
The assertion was a `node -e` heredoc inside a `run:` block, and a comment in it contained backticks. The shell read those as command substitution and the step died with `unexpected EOF while looking for matching \``, exit 2, before anything was asserted — while the log filled with `validate`'s own warnings, which is what a reader would have blamed. I did blame them, and spent a while looking for violations that were never there. It is `scripts/assert-dogfood.mjs` now. A file has no quoting layer, so nothing in it can be reinterpreted on the way to Node, and the step is two lines. The assertion itself is unchanged: violations, secrets, and both `shape` and `reference` reporting `ok`. `not-checked` still fails, which is the state this gate exists for. Limit: this checks the report `validate` produced; it does not re-derive the range, so a range that silently covered nothing would pass here Blast: local Undo: easy Certainty: firm Verified: run against a real report it prints `violations: none`, `shape: ok`, `reference: ok` and exits 0; with the reference status set to `not-checked` it names the reason and exits 1; the workflow parses and the action lint passes Provenance: authored Record-Id: r-assertfile1
The dogfooding gate fetched the notes mirror for the first time and its reference check ran for the first time with it. It found one violation, in this repository's own history: 03b4bfe carries `Follows: r-8c31f7`, and `r-8c31f7` was not written until 53d60c2, six minutes later, by somebody who had noticed the omission — the commit subject says exactly that. The record exists and is reachable from HEAD; the reference still points forward, which is what the check reports. It lives in a commit message, so it cannot be corrected without rewriting history. Two ways to make the job green: widen the check, or record the case. Widening it would retire the check that #542 asked for on the day it first did its job. So the exception is a file — sha, rule, value, and why — and the assertion subtracts only what that file names. Anything else still fails. The count of carried violations is printed on every run, so the exception stays visible rather than becoming the silence it replaced. Verified: `node scripts/assert-dogfood.mjs` over the full adoption range exits 0 and prints `reference: ok` with the entry present, and exits 1 on a violation the file does not name Evidence: scripts/dogfood-baseline.json Evidence: scripts/assert-dogfood.mjs Follows: r-9c07e2 Blast: module Undo: easy Certainty: firm Record-Id: r-4e91ab Provenance: authored CommitLore-Version: 2.0.0
There was no way to report one privately. GitHub's private reporting was off,
so the only channels were a public issue — which publishes the vulnerability at
the moment it is disclosed — or an email address nobody had. Private reporting
is now enabled on the repository and SECURITY.md points at it, so a draft
advisory stays between reporter and maintainer until there is a fix.
The scope section is the part worth reading. CommitLore's security surface is
not the usual one: it serves recorded text to agents that act on it, so the
questions are whether a record can reach an agent as an instruction, whether a
trust grade can overstate what it knows, and whether capture can write a record
it was never handed evidence for. Those are named as reportable, along with the
install path, which writes host configuration. What is deliberately not
reportable is named too — a stale index is a derived cache (ADR-0003), and
`[claim]` content being wrong is the grade doing its job, not a bug.
Dependabot covers npm and the workflow actions, monthly and grouped. Weekly
per-package PRs get closed unread, and the failure this guards against is a
pinned action going stale until a gate stops meaning what it says. Security
updates ignore both the schedule and the grouping.
Verified: private vulnerability reporting reads back `{"enabled": true}`; a
Dependabot-shaped trailerless commit validates with 0 violations and
`shape: ok`, so these will not trip the dogfooding gate
Evidence: SECURITY.md
Evidence: .github/dependabot.yml
Follows: r-4e91ab
Blast: system
Undo: easy
Certainty: firm
Record-Id: r-7d20c5
Provenance: authored
CommitLore-Version: 2.0.0
…ed like one The release gate matched required checks by name alone. A check run's name is chosen by whoever creates it, and any GitHub App installed on the repository can create one: name it `check (22)`, conclude it `success`, point it at the release SHA. The gate read that as this repository's CI having passed and let publication through. Closes #571. The run carries its producer. `app.slug` is GitHub's identifier for the app that opened the check run, set by GitHub rather than by the caller, so requiring `github-actions` distinguishes a CI result from something merely shaped like one. A run with no app attributed is refused for the same reason — absent provenance is not proof of provenance. A foreign run is reported rather than filtered away. Dropping it silently would leave the gate saying "required check is absent", which is true but hides that something posted under that check's name. Both facts are stated. Verified: the four new cases fail against the previous name-only match — a look-alike is accepted as the genuine check, and a forged duplicate alongside a real success passes — and pass with the producer required; 19/19 in the file Evidence: scripts/check-exact-head-ci.mjs Evidence: test/release-publish-prerequisites.test.ts Follows: r-7d20c5 Blast: system Undo: easy Certainty: firm Record-Id: r-b6f3e8 Provenance: authored CommitLore-Version: 2.0.0
`npm audit` reported ten vulnerabilities on every CI run — six moderate, three high, one critical — and the job was green every time. Nobody could tell "assessed and accepted" from "never looked at", because both look like a warning nobody acts on. Closes #545. Splitting the number answers it. What users receive is `dist/`, committed per ADR-0011 and cloned by the installer without npm ever running, so an installed CommitLore has no `node_modules` at all. The production audit is therefore the count that can reach a user, and it is zero — all ten live in build and test tooling. That is now the blocking half, at `--audit-level=low` so zero means zero rather than "nothing above a threshold I picked". The full audit still runs and still prints, without failing a build no installation depends on; Dependabot is what moves those forward. The split is only honest while the premise holds, so the premise is checked too: `dist` and `spec` are copied somewhere with no `node_modules` and the runtime is asked for its version. If the bundle ever stops being self-contained, the production audit quietly stops covering what users run — this fails first. Verified: `npm audit --omit=dev --audit-level=low` exits 0 while the full audit reports 10; the copied tree has no node_modules and answers `0.8.1`. The self-contained claim was checked further by hand — validate, an MCP handshake, tools/list, and a prepare_capture call all run from a clone with no dependencies installed; the `from 'ajv'` strings in the bundle are JSDoc examples inside the vendored SDK, not imports Evidence: .github/workflows/ci.yml Follows: r-b6f3e8 Blast: system Undo: easy Certainty: firm Record-Id: r-9f04d7 Provenance: authored CommitLore-Version: 2.0.0
An installation missing `spec/` failed every commit with a raw ENOENT and a usage line underneath. The two things on screen were a path the user never chose and usage for a command they never typed, so the available reading was "my trailers are wrong" — and the message had not been examined at all. The hook is where people meet this, which is why it cost an hour of editing a commit message that was already correct. Closes #533. `readInstalledFile` now distinguishes a missing shipped file from any other read failure and says three things: what is absent, that the message was not examined, and the command that restores it. Nothing the caller could retype fixes this, so no usage line is printed. The exit code separates too: 3 for an operational failure, distinct from 2 for usage. That is the first code from the taxonomy #543 asks for, taken here because #533 is where the conflation actually reaches a user. The rest of that taxonomy — NoRecord, RecordRejected, InternalError, shared across the CLI, the hooks and the MCP server — is still open and is not attempted in a release cut. The gate's producer check from the previous commit had a second call site. `release-tag-binding` builds its own check-runs payload and was refused by the stricter gate; CI caught what running one test file did not. Both payload builders are now the only two, confirmed by search. Verified: with `spec/` removed, a real `git commit` through the commit-msg hook prints only those three things, creates no commit, and validate exits 3; four cases assert it against a copied installation tree, two of which fail against the previous routing Evidence: src/core/paths.ts Evidence: src/commands/validate.ts Evidence: test/validate.test.ts Follows: r-9f04d7 Blast: module Undo: easy Certainty: firm Record-Id: r-2c8e5a Provenance: authored CommitLore-Version: 2.0.0
The 0.8.1 entry stopped at the point the reviews had reached. Since then the reference check ran for the first time and found something, the release gate learned to ask who reported a check, the audit number was split into the two questions it was answering, and a broken installation stopped reading as a bad commit message. A reader deciding whether to upgrade needs those. The dogfood baseline is stated as a carried violation with its reason rather than left implicit. An exception nobody can see is the thing it replaced. Verified: the entry names each change against the commit that made it Evidence: CHANGELOG.md Follows: r-2c8e5a Blast: local Undo: easy Certainty: firm Record-Id: r-5a71bc Provenance: authored CommitLore-Version: 2.0.0
The baseline subtracted the recorded violation from the list and left `validate`'s own `reference: failed` standing, so the gate went red for the one case that had been named and explained. CI said it plainly — `1 carried`, `shape: ok`, `ERROR: reference is failed` — and it was right to. I checked the baseline against a report generated earlier, which still said `reference: ok`. That is the whole mistake: the fix was verified against a stale artefact instead of the one the gate produces. The report CI builds is the only one that answers the question. A failed class is now tolerated when the subtraction is its entire explanation — nothing unrecorded remains and something was in fact carried — and the line saying so is printed. `not-checked` is never tolerated, whatever the baseline holds: a sub-check that did not run is precisely what #542 was about, and no recorded exception excuses one. This logic had no tests, which is why the flaw shipped at all. It has nine now, including the exact report CI produced. Verified: removing the tolerance fails the two cases that model CI's report; widening it to cover `not-checked` fails the case guarding #542; restored, 9/9 pass Evidence: scripts/assert-dogfood.mjs Evidence: test/assert-dogfood.test.ts Follows: r-5a71bc Blast: module Undo: easy Certainty: firm Record-Id: r-8b3f16 Provenance: authored CommitLore-Version: 2.0.0
…bered This release added `audit`, `install-macos` and two `install-alpine` jobs to CI. The release gate's required list was not touched, so it kept qualifying releases on the six checks it already knew. Four jobs could have failed at a tagged commit while the gate reported every required check green — the same shape as #571 one layer up: the gate looked strict and was checking less than it appeared to. The list stays fixed rather than inferred. A list built from what reported at a SHA lets a check that failed to report define itself out of the requirement, which is the failure the gate exists for. What was missing is something that notices when CI grows past it, so two cases compare the list against ci.yml's jobs in both directions: a job with no entry fails, and an entry naming a job that does not exist fails too — the second because a check that cannot run can never be present, which would block every release. `lint` is deliberately excluded. Its job is conditioned on `github.event_name == 'pull_request'`, so it does not run on the push to main that produces the checks at a release commit. Confirmed against the current main commit, which carries exactly six check runs, all from `github-actions`. Verified: dropping `install-macos` from the list fails the first case; adding a name ci.yml does not define fails the second; restored, 55/55 across both release-gate files. The count in the tag-binding assertion now derives from the list rather than repeating a literal six Evidence: scripts/check-exact-head-ci.mjs Evidence: test/release-publish-prerequisites.test.ts Follows: r-8b3f16 Blast: system Undo: easy Certainty: firm Record-Id: r-3f6d09 Provenance: authored CommitLore-Version: 2.0.0
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.
0.8.0 shipped a protocol whose server described half of itself.
The gap
The MCP server's
instructionscovered the read half — read the context resource before editing, how to read the trust tiers — and said nothing about when to record. Four of the seven hostsinstall.shwires receive anmcpServersentry and no skills:Those four held the capture tools with nothing telling them what the tools were for.
AGENTS.mdwas the only channel carrying the missing half — which is whyinitwrote it into repositories whether or not they use the convention.The fix
The server describes both halves. It ships to every host by definition; a file in somebody's repository does not.
Proven with the plugin disabled (no skill loaded) and no
AGENTS.mdanywhere:That is the skill-less path — the one those four hosts are on.
Writing
AGENTS.mdis now--agents-md, off by default.Also
A registration naming a path that does not exist is reported instead of counted as healthy. Four hosts on this machine pointed at
/tmp/fresh256…/bin/commitlore, a temp directory deleted long ago, and every reinstall said they were fine. The file is still never rewritten.Verification
Version consistent across all three manifests, both lockfile fields and the built CLI at
v0.8.1; 30 pins updated across four READMEs and both installers. 199 pass across readme, manifest, check-release-version, init, mcp and both installer suites.