Skip to content

Ship CommitLore as a compiled single-executable binary that needs no Node (#39) - #94

Merged
MongLong0214 merged 3 commits into
devfrom
feat-issue-39
Jul 28, 2026
Merged

Ship CommitLore as a compiled single-executable binary that needs no Node (#39)#94
MongLong0214 merged 3 commits into
devfrom
feat-issue-39

Conversation

@MongLong0214

@MongLong0214 MongLong0214 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Ships dist/commitlore, a compiled Node Single Executable Application that needs no Node runtime, no interpreter, and no node_modules at all. doctor, validate, context, guard, inject, and index --rebuild all run against PATH=/usr/bin:/bin. dist/commitlore.mjs and ADR-0011's "git clone is the whole install" are unchanged — the binary is a second, uncommitted, reproducible build artifact (npm run build:binary), not a replacement channel.

See docs/adr/ADR-0015-single-executable-binary.md for the full decision record.

Approach chosen, and what was ruled out

Chosen: Node SEA (--experimental-sea-config + postject), building from the same src/cli.ts entry dist/commitlore.mjs already bundles. node:sqlite (ADR-0012) was the last native dependency blocking this — it's a Node builtin, not a .node addon, so nothing about being embedded in a SEA blob changes how it loads, which this PR confirms rather than assumes.

One correction along the way, verified empirically rather than trusted from a doc summary: the SEA main script must be CommonJS on this Node line. An ESM main (mainFormat: "module", a field this Node line's schema doesn't even define) fails blob generation with Cannot use import statement outside a module, and the same file run directly as an SEA main fails identically at runtime. The shipped @types/node doc agrees once actually read. scripts/build-binary.mjs bundles src/cli.ts a second time to CommonJS as a build intermediate — a temp file, never committed — so dist/commitlore.mjs never changes.

  • Ruled out: mainFormat: "module" — see above; verified to fail, not merely undocumented.
  • Ruled out: pkg / nexe — third-party bundlers embedding a separate, forked Node runtime this project doesn't control the patch cadence of; pkg is archived upstream. Trades the runtime dependency this ticket removes for a worse one.
  • Ruled out: Deno compile / Bun compile — a different runtime. node:sqlite, the TypeScript, and NodeNext resolution are all Node-specific; retargeting them is a second runtime port, not a build step.
  • Ruled out: reimplement in Go/Rust — issue B-09 · Single static binary — remove the Node runtime dependency #39's own second option, real via spec/fixtures/ + spec/contract-cases/, but an order of magnitude more work than this ticket and not needed to solve either problem (latency, no-Node-on-PATH) it opens with.
  • Ruled out: committing dist/commitlore next to dist/commitlore.mjs — see "Distribution decision" below.
  • Ruled out: Windows (commitlore.exe) in this PR — Node's docs describe a signtool path this repo has no CI runner to verify. classifyBinTarget and the hook resolution order are written so it's a small additive follow-up, not a redesign.

Distribution decision — the binary is not committed

At ~115 MiB, platform- and architecture-specific, and not meaningfully diffable, committing it next to dist/commitlore.mjs would break ADR-0011's "committed dist/ matches src/, byte for byte" CI invariant, and — the irreversible part — a pushed blob that size is never removable from git history again. It is never staged: .gitignore excludes /dist/commitlore, verified with git add -A -n before committing (no match) and git ls-files | grep dist/commitlore after pushing (no match).

Instead: npm run build:binary builds it locally and reproducibly, and CI (this PR) now builds and smoke-tests it on ubuntu-latest and macos-latest on every push, without ever committing it — the same role issue #39's own AC pointed at ("attach platform binaries to the release"). docs/adr/ADR-0011-plugin-first-distribution.md gets a short "Addressed" callout pointing here; its own decision ("distribution is git clone") is unchanged.

The no-Node run

Verified directly against the real compiled binary, immediately before the commit that ships this PR, with env -i PATH=/usr/bin:/bin:

$ env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore doctor
ok      cli runtime — running as a compiled binary at /path/to/dist/commitlore (commitlore 0.1.0)
...
$ printf 'Subject\n\nBlast: wide\n' | env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore validate
shape failed · references not checked (no repository)
3: enum Blast — got "wide", want "local|module|system"
commitlore: 1 violation (SPEC §6) — the message was not modified
exit=1

$ env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore index --rebuild
rebuilt: scanned 1 commit, indexed 4 trailers in 123ms

$ env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore context file.ts
context for file.ts as of ... — 0 limits, 0 ruled-out, 0 warnings, 3 other in 1 record (index, 1 commit record(s) scanned)

$ echo "$payload" | env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore guard --hook-input
(exit 0, silent — no risky proposal)

$ echo "$payload" | env -i PATH=/usr/bin:/bin HOME="$HOME" ./dist/commitlore inject --hook-input
{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"commitlore: active records for file.ts\n\nOther\n  [claim] ..."}}

index --rebuild confirms node:sqlite genuinely works inside the SEA blob, not just in theory. CI's new binary job runs this exact sequence on ubuntu + macos on every push.

Hook overhead, against +102.40 ms

bench/binary-hook-overhead.ts re-measures the PreToolUse inject hook same-session, three arms (no hook / node dist/cli.js / the binary) against the same repeated file write, matching bench/deterministic/hooks.ts#preToolUseOverhead's method (one discarded warmup, 20 timed runs).

The machine this PR was built on was shared with other concurrent work for part of the session (load average briefly 100–150). The number below is from a run taken and re-confirmed with uptime at load average 13–20, not the contaminated ones — those were discarded rather than reported.

arm p50 ms p95 ms
no hook (baseline) 0.06 0.09
node dist/cli.js inject --hook-input 127.13 132.63
dist/commitlore inject --hook-input 68.24 71.77

Node-path delta: 127.07 ms (consistent with the committed +102.40 ms figure — same order of magnitude, not an identical machine state). Binary delta: 68.18 ms — 46.3% lower than the node path, same session. Repeated three times total across this session at calm load; every run landed in the 66–68 ms band for the binary and 122–127 ms for the node path.

This is a real, bounded reduction, not elimination — the answer the ticket asked for either way. SEA still pays V8 startup and blob decompression; it just skips a second process's worth of module resolution on top of that. Full numbers: bench/results/binary-hook-overhead-*.md.

#71's containment guarantee, extended and re-verified

src/hooks/commit-msg.ts's resolution order gets a second branch for a binary: no .js/.mjs extension, no separate interpreter (core/hook-target.ts#classifyBinTarget, keyed on the name commitlore, not merely "no extension" — that would allow-list every other executable on the machine). Containment is an exact match against the recorded install (commitlore.root) instead of a directory prefix, since a binary has no subdirectory for a foreign file to hide in.

Both of #71's attacks were re-run against the real compiled binary and its installed commit-msg hook, immediately before the commit:

$ ./dist/commitlore hooks install
$ git config --local commitlore.bin /path/outside/root/commitlore   # attack 1: outside install root
$ git commit -m "Attack1..."
commitlore: cannot find the CLI this hook was installed with.
commit exit=1

$ ./dist/commitlore hooks install   # reinstall, then attack 2
$ ln -s /path/outside/root/commitlore "$(dirname "$root")/commitlore-symlink"
$ git config --local commitlore.bin "$symlink_path"   # attack 2: symlink inside root, pointing out
$ git commit -m "Attack2..."
commitlore: cannot find the CLI this hook was installed with.
commit exit=1

$ ./dist/commitlore hooks install   # legitimate install still works right after
$ git commit -m "Legit..."
commit exit=0

Both attacks refused; the legitimate install validates a bad message (rejected) and a good one (accepted) immediately after. A new regression test for the symlink attack (which had no automated test before this PR, for either the script or binary branch) is in test/hooks.test.ts, plus a full a compiled-binary-shaped entry point describe block covering both attacks, the happy path, and COMMITLORE_BIN targeting a binary. test/hook-target.test.ts is new — unit coverage for classifyBinTarget and readRecordedHookTarget's problem list. CI's new binary job re-runs both attacks against the real compiled binary on every push (ubuntu + macos).

Follow-up (same branch, same PR): the release pipeline

#39's AC also asked to "attach platform binaries to the release," which the
commit above deliberately left for a separate act — the binary stayed
uncommitted and CI only builds/smoke-tests it on push. Two more commits on
this branch add the actual publishing side: .github/workflows/release.yml,
install.sh, and READMEs updated in all four languages. Nothing about the
SEA decision above changes; this builds on top of it.

.github/workflows/release.yml — tag-triggered, nothing else

on: push: tags: ["v*"] only. No schedule, no branches, no
workflow_dispatch — cutting a release is an owner action, and pushing the
tag is the only approval this workflow recognizes.

Three jobs:

  1. version-consistency (new scripts/check-release-version.mjs) —
    the tag, package.json's version, and commitlore --version must all
    agree, checked before any binary is built. Verified against this tree
    both ways: v0.1.0 passes (all three agree at 0.1.0); a deliberately
    wrong v9.9.9 fails with both disagreements listed and exit 1.
  2. build matrix — macos-latest (arm64), macos-13 (x64),
    ubuntu-latest (x64), ubuntu-24.04-arm (arm64) — packages
    commitlore-<version>-<target-triple>.tar.gz (the Rust-ecosystem
    naming convention install.sh parses) and attests each asset with
    actions/attest-build-provenance (Sigstore-backed, keyless, no key
    this project manages).
  3. publish — one SHA256SUMS covering all four assets together,
    attested too, then gh release create --generate-notes.

Linux arm64 needed no emulation. GitHub's own ubuntu-24.04-arm hosted
runner is available to this (public) repo — confirmed by actually building
and running the SEA binary on it via a throwaway probe branch
(probe/windows-sea, workflow_dispatch + push triggered, deleted after):
uname -maarch64, npm run build:binary succeeded, --version and
doctor both ran clean.

Windows is deliberately not in the matrix — verified absent, not assumed
absent.
The same probe branch ran on windows-latest and reproduced two
real, reproducible failures, not merely "no runner yet":

  1. npm run build:binary fails before SEA is even reached.
    scripts/build-binary.mjs's REPO_ROOT is resolve(new URL('..', import.meta.url).pathname); a Windows file:// URL's .pathname keeps
    its leading slash in front of the drive letter, and win32
    path.resolve doubles it: the CI log shows literally
    D:\D:\a\commitlore\commitlore\dist\commitlore.mjs does not exist. Not
    a one-off — scripts/check-engines.mjs has the identical pattern, so
    it's a systemic gap in the scripts/ layer, not one missed line.
  2. Even past that: classifyBinTarget (src/core/hook-target.ts) and the
    commit-msg shell stub's case patterns recognize a compiled binary
    only by the exact basename commitlore — never commitlore.exe, which
    is what build-binary.mjs's own OUTPUT already names the Windows
    build. The installed hook would silently fall through Security: commitlore.bin and COMMITLORE_BIN are executed, and the env path lacks the guard the config path has #71's containment
    check rather than refuse to resolve it, on the platform that most needs
    that check to hold. ADR-0015 already named this "a small, additive
    follow-up" — confirmed empirically here, not fixed here: fixing either
    of these is a source change to this PR's own shipped hook-target/
    build-binary logic, out of scope for the release-pipeline change that
    surfaced them. Windows stays out until both are fixed and re-verified on
    a real Windows runner.

install.sh — the second install path, checksum-verified before anything runs

curl -fsSL https://github.com/ghraw/MongLong0214/commitlore/dev/install.sh | sh

Detects OS/arch, downloads SHA256SUMS from the release first (fixed
filename, no version-guessing, no GitHub API call), resolves this
platform's asset out of it, and verifies the checksum before installing
anything
. Honors $PREFIX or $COMMITLORE_INSTALL_DIR; refuses to
overwrite a file it did not install (checked by output shape — "runs and
exits 0" is not enough, plenty of unrelated executables do that for an
unrecognized flag; a caught bug during testing, see below). Fails loudly
on any OS/arch that isn't published.

Verified end-to-end against a real, locally built SEA binary packaged
exactly as release.yml would, served over a local python3 -m http.server standing in for GitHub's release-asset URLs (no real release
exists yet — that's the owner's action):

  • Happy path: checksum verified, installed, --version reports 0.1.0.
  • Re-run over itself: recognized as its own prior install, upgraded.
  • Hand-edited SHA256SUMS to a wrong hash: refused before installing
    anything, exit 3, nothing written.
  • Caught and fixed a real bug: the first version of the
    don't-overwrite-a-foreign-file check trusted <file> --version exiting
    0 alone, and silently overwrote a throwaway foreign script that printed
    unrelated text and exited clean. Fixed to check the output actually
    looks like this CLI's own ([0-9]*.[0-9]*.[0-9]*); re-verified with the
    identical foreign file — correctly refused, exit 4, foreign file
    untouched.
  • Faked uname -s/uname -m for an unsupported OS and an unsupported
    arch — both fail loudly, exit 1, before any network call.
  • sh -n and dash -n (strict POSIX) both accept the script; the full
    happy path re-run under dash directly gives identical results.
  • The manual (non-piped) verify sequence added to all four READMEs
    (curl -fsSLO + grep ... | shasum -a 256 -c - / sha256sum -c -)
    produces "OK" against the same hand-built asset.

READMEs, all four languages, and package.json

Curl one-liner + manual verify steps added to README.md/.ko.md/.ja.md/
.zh-CN.md, so piping to a shell is never the only documented path in any
language this project ships docs for. While there: PR #94 had only added
"### Run as a compiled binary, without Node installed" to README.md, so
the three non-English READMEs were both missing that section and still
listing issue #39 under "not done yet." Backfilled together, since the new
release-install section reads as non-sequitur without the section it
follows in English.

package.json gets "private": true — issue #93's own note that this is
"worth a single cleanup once #39 lands rather than twice," and #39 has now
landed both halves. bin/files/runtime dependencies are #93's other
findings, intentionally untouched here (separate blast radius); private: true is the one that's a direct, structural guarantee that npm publish
can't ever re-happen by accident.

What's still unverified, plainly

The entire tag-triggered flow — the real gh release create, the real
attestation publish, all four platforms' binaries actually landing on a
release — has never run end to end, because cutting a release is
explicitly not this PR's action to take. Everything above is what this
repository lets a change verify before a tag exists: real runners (via a
throwaway, now-deleted probe branch, not this workflow), the
version-consistency script against real passing/failing input, the install
script against a hand-built asset and hand-made checksums, and the
workflow file's own YAML validity (python3 -c "import yaml; yaml.safe_load(...)"
— no act/actionlint available in this environment, so job-graph/
expression-level linting is unexercised). The first real tag is where the
rest gets checked.

Tests

npx vitest run1357 passed, 1 skipped (pre-existing, COMMITLORE_PERF_LARGE-gated), 37/37 files, 0 failed. test/mcp.test.ts flaked once under concurrent load from other work on the shared machine mid-session (issue #88, exactly as expected) and passed cleanly alone and in the final full run after that load cleared.

Independently re-confirmed before the release-pipeline commits (not merely re-quoted): npx vitest run --reporter=default on this exact tree — Test Files 37 passed (37), Tests 1357 passed | 1 skipped (1358). Same numbers, run fresh.

Also green: npm run typecheck, npm run build (rebuilt dist/ is byte-identical, confirmed twice), bash spec/verify.sh, node scripts/check-engines.mjs, node scripts/check-readme-numbers.mjs — all re-run again after the release-pipeline commits, still green.

Each commit passes node dist/commitlore.mjs validate --commit HEAD, including the two added on top for the release pipeline.

Test plan

  • env -i PATH=/usr/bin:/bin run of doctor, validate, context, guard, inject, index --rebuild against the real binary
  • Hook overhead re-measured against +102.40 ms, at confirmed-calm load, three consistent runs
  • dist/commitlore.mjs unchanged and still works (existing fresh-clone CI step untouched)
  • Security: commitlore.bin and COMMITLORE_BIN are executed, and the env path lacks the guard the config path has #71's two attacks re-run against the real binary + installed commit-msg hook — both blocked
  • dist/commitlore never staged or pushed (git add -A -n / git ls-files both confirm)
  • CI: new binary job (ubuntu + macos) builds the binary and runs the no-Node + Security: commitlore.bin and COMMITLORE_BIN are executed, and the env path lacks the guard the config path has #71 checks
  • npx vitest run — 1357/1358, 37/37 files, 0 failed (re-confirmed independently before the release-pipeline commits)
  • release.yml parses as valid YAML; job/matrix structure reviewed by hand (no act/actionlint available)
  • scripts/check-release-version.mjs verified against real matching input and a deliberate mismatch
  • ubuntu-24.04-arm confirmed as a real, non-emulated hosted runner (throwaway probe, deleted)
  • windows-latest confirmed broken for this build (throwaway probe, deleted) — excluded from the matrix with the exact failure documented, not guessed at
  • install.sh verified end-to-end against a real local SEA build + hand-made SHA256SUMS: happy path, upgrade, checksum mismatch, foreign-file refusal (bug found and fixed here), unsupported OS/arch, dash -n strict-POSIX check
  • All four READMEs' manual verify snippets tested directly, not merely written
  • The real tag-triggered flow end to end (gh release create, real attestations, all four assets landing on an actual release) — not run. Cutting a release is the owner's action; this PR builds the machinery and leaves it unused, exactly as asked.

Not done, on purpose: no tag was pushed, no release was cut, no binary or archive was committed, npm publishing was not re-added (the opposite — private: true closes it structurally per #93), and #71's containment / the existing CI jobs were left untouched.

…Node (#39)

`npm run build:binary` (`scripts/build-binary.mjs`) builds `dist/commitlore`
with Node's own `--experimental-sea-config` + `postject` from the same
`src/cli.ts` entry `dist/commitlore.mjs` already bundles. `node:sqlite`
(ADR-0012) was the last native dependency blocking this — it is a Node
builtin, not a `.node` addon, so nothing about embedding it in a SEA blob
changes how it loads, which #39 is now able to confirm rather than assume.

Node's SEA main script must be CommonJS on this Node line — verified
empirically, not read off a doc and trusted: an ESM main (`mainFormat:
"module"`, a field this Node line's schema does not even define) fails blob
generation with "Cannot use import statement outside a module", and the same
file run directly as an SEA main fails identically at runtime. The shipped
`@types/node` doc agrees once actually read: "the single executable
application feature currently only supports running a single embedded script
using the CommonJS module system." `build-binary.mjs` bundles `src/cli.ts` a
second time to CommonJS as a build intermediate — a temp file, never
committed — so `dist/commitlore.mjs` never changes format or content.

A compiled binary has no directory tree beside it: Node's docs say
`import.meta.url`/`__dirname` inside an SEA resolve to the executable's own
path, not a directory containing `spec/` or `package.json`. `core/paths.ts`
now branches on `sea.isSea()` — `PACKAGE_ROOT` stays a real, non-throwing
directory so the module graph doesn't crash at import time, and
`readInstalledFile` routes to `sea.getAsset` for the three files
`build-binary.mjs` embeds (`package.json`, `spec/SPEC.md`,
`spec/schema/record.schema.json`) instead of the filesystem. `index-db.ts`'s
`createRequire(import.meta.url)` for `node:sqlite` moved to
`createRequire(process.execPath)` for the same reason, one level down: a CJS
bundle's `import.meta` is empty, and `node:sqlite` is a builtin so the base
path never needed to be meaningful in the first place.

`node:sea` also could not be statically imported (`import ... from
'node:sea'`) without breaking every test file that transitively imports
`core/paths.ts` — Vite/vitest's SSR module graph externalizes a `node:`
specifier by checking `node:module`'s `builtinModules` list, which does not
carry `node:sea` yet, and mis-resolves it as a package literally named `sea`.
`createRequire(process.execPath)('node:sea')` sidesteps the bundler's older
check the same way `index-db.ts` already sidesteps it for `node:sqlite`;
`import type` keeps the real signatures for the cast at zero runtime cost.

The commit-msg hook's resolution order (#71) gets a second branch. A binary
has neither a `.js`/`.mjs` extension nor a separate interpreter — it is
recognized by name (`commitlore`) rather than "no extension", which would
allow-list every other executable on the machine the moment it lost the
extension check. Its containment check is an exact match against the
recorded install (`commitlore.root`) rather than a directory prefix: a
binary has no subdirectory for a foreign file to hide in, it is the whole
install. `hooks.ts#recordBinPath` needed one line of new logic for this
(recording `commitlore.root` as the binary's own realpath for a binary-kind
entry) — Node's SEA docs describe `process.argv[1]` inside a real SEA binary
as already being the executable's own path, so the existing line that reads
it needed no branch at all. `scripts/commitlore-run.sh` (the Claude Code
plugin's PreToolUse hook) now tries the binary before its `command -v node`
gate, fixing a pre-existing gap in the same motion: previously a `commitlore`
resolved on PATH was never even attempted when node was absent, regardless
of whether it needed node at all.

Distribution: `dist/commitlore.mjs` and ADR-0011's "git clone is the whole
install" are unchanged. The binary is not committed next to it — at ~115 MiB,
platform- and architecture-specific, and not meaningfully diffable, it would
either break ADR-0011's "committed dist/ matches src/, byte for byte" CI
invariant or need an exception carved out of it, and every rebuild would
rewrite the whole blob in history forever with no way to remove it again.
`npm run build:binary` produces it locally and reproducibly instead, and CI
now builds and smoke-tests it (ubuntu + macos) on every push without ever
committing it — the same role issue #39's own acceptance criteria pointed at
("attach platform binaries to the release").

Ruled-out: mainFormat: "module" (an ESM SEA main) | verified to fail both
  blob generation and runtime on this Node line, not merely documented as
  unsupported (see above)
Ruled-out: pkg / nexe | third-party bundlers embedding a separate, forked
  Node runtime this project does not control the patch cadence of; pkg is
  archived upstream. Trades the Node runtime dependency this ticket removes
  for a different, less-maintained one
Ruled-out: Deno compile / Bun compile | a different runtime. node:sqlite,
  the TypeScript, and NodeNext resolution are all Node-specific; retargeting
  them is a second runtime port, not a build step, and issue #39's own
  first option ("Node SEA -- no source rewrite") needs none
Ruled-out: reimplement in Go/Rust | issue #39's own second option, and a
  real one via spec/fixtures + spec/contract-cases, but an order of
  magnitude more work than this ticket and not needed to solve either
  problem (latency, no-Node-on-PATH) this ticket opens with
Ruled-out: committing dist/commitlore next to dist/commitlore.mjs | breaks
  ADR-0011's committed-dist/-matches-src/ invariant at ~115 MiB per
  platform/arch, and a pushed blob that size is not removable from git
  history again
Ruled-out: Windows (commitlore.exe) in this PR | Node's docs describe a
  signtool path this repository has no CI runner to verify; shipping an
  unverified platform claim is what this project's numbers-or-silence
  discipline exists to refuse. classifyBinTarget and the resolution order
  are written so it is a small additive follow-up, not a redesign
Warn: node:sea is "Active development" per Node's own docs; its schema or
  CommonJS-only constraint could change between Node versions.
  core/paths.ts's readInstalledFile/isSea split and build-binary.mjs's
  asset map are the one place that assumption is absorbed, same posture as
  ADR-0012 already committed to for node:sqlite
Limit: doctor's PreToolUse hook runtime check still shells to
  scripts/commitlore-run.sh via bash for its own probe; a binary install
  with the Claude Code plugin hook already wired reports a plain
  ENOENT-style fail there rather than trying the binary directly -- not
  one of #39's six required commands, not fixed here
Verified: doctor, validate, context, guard, inject, and index --rebuild all
  run against env -i PATH=/usr/bin:/bin with the real compiled binary,
  re-run immediately before this commit against the current tree
Verified: both of #71's attacks re-run against the real binary and its
  installed commit-msg hook immediately before this commit -- commitlore.bin
  pointed at a copy of the binary outside the install root, and a symlink
  planted inside the install root pointing at that same outside copy. Both
  refused ("cannot find the CLI"); a legitimate binary install still
  validates a bad message (rejected) and a good one (accepted) right after
Verified: hook overhead re-measured same-session (bench/binary-hook-overhead.ts,
  three arms against the same repeated file write), three times with load
  average 13-20 on a machine shared with other concurrent work this
  session: node dist/cli.js p50 122-127ms (consistent with the committed
  +102.40ms figure this ticket cites), binary p50 66-68ms -- 46-47% lower
  than the node path every time, same session. A real, bounded reduction,
  not elimination: SEA still pays V8 startup and blob decompression, just
  not a second process's module resolution on top of it. Numbers in
  bench/results/binary-hook-overhead-*.md
Verified: npx vitest run -- 1357 passed, 1 skipped (pre-existing,
  COMMITLORE_PERF_LARGE-gated), 37/37 files, 0 failed
Verified: npm run typecheck, npm run build (dist/ byte-identical on
  rebuild), bash spec/verify.sh, node scripts/check-engines.mjs, and node
  scripts/check-readme-numbers.mjs all pass
Blast: system
Undo: easy
Certainty: firm
Evidence: src/core/paths.ts
Evidence: src/core/hook-target.ts
Evidence: src/core/index-db.ts
Evidence: src/core/schema.ts
Evidence: src/core/harvest.ts
Evidence: src/hooks/commit-msg.ts
Evidence: src/commands/hooks.ts
Evidence: src/commands/doctor.ts
Evidence: scripts/build-binary.mjs
Evidence: scripts/commitlore-run.sh
Evidence: bench/binary-hook-overhead.ts
Evidence: .github/workflows/ci.yml
Evidence: docs/adr/ADR-0015-single-executable-binary.md
Evidence: test/hook-target.test.ts
Evidence: test/hooks.test.ts
Evidence: test/doctor.test.ts
Follows: r-adr0012
Record-Id: r-seabin39
Provenance: authored
CommitLore-Version: 2.0.0
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

CommitLore — record lint

Trailers: clean — 3 commits in origin/dev..c6e1d0424d2966c85f5e2341bb9c0540a1cd4555
Active constraints: 44 limits · 111 ruled-out · 67 warnings — from 81 records over 49 changed paths

Active constraints for the paths this PR touches

Limits (44)

  • 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-seabin39 9e9cd0e — doctor's PreToolUse hook runtime check still shells to scripts/commitlore-run.sh via bash for its own probe; a binary install with the Claude Code plugin hook already wired reports a plain ENOENT-style fail there rather than trying the binary directly -- not one of B-09 · Single static binary — remove the Node runtime dependency #39's six required commands, not fixed here
  • 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-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-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-3b8f52 1f8b4be — the figure is one machine, one run — it establishes the order of magnitude, not a regression baseline anyone should tune against
  • 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-6e1a72 5e09846npx commitlore is the first thing a reader will try, and it fails until the package is published
  • r-0c5d38 aeb54a6 — the suite runs against the source tree, so no test in it can observe what packaging drops
  • 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-6f2e58 ea9ae6d — a library test and a binary test cover different failures, and the packaging layer between them is exactly where a working module becomes a broken install
  • r-7e5f02 e5f5e00 — npm installs through an engine mismatch, so the ecosystem's own signal cannot be relied on to stop anything
  • r-3a9d68 6a3fc3b — a test runner reports what ran, and nothing in its summary distinguishes "did not run" from "does not exist"
  • 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-8e2d51 ef93c0e — git is the parser, so a git version that folds or bounds trailers differently is a correctness problem for us, not a compatibility footnote
  • 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 (111)

  • r-relinstall c6e1d04 — guessing the current version to build the asset URL directly | would need either the GitHub API (rate-limited, needs no-auth headers handled correctly) or trusting a redirect's final Location header parsing. Downloading the fixed-URL SHA256SUMS first and reading the real asset name back out of it needs neither and is what the checksum step has to fetch anyway.
  • r-relinstall c6e1d04local for scoping — not POSIX per se, but supported by dash, bash, and every shell this script is realistically piped into (verified directly, see Verified) | not used in the end; the script has few enough variables that scoping was not needed, only noted here because it was considered.
  • r-seabin39 9e9cd0e — mainFormat: "module" (an ESM SEA main) | verified to fail both blob generation and runtime on this Node line, not merely documented as unsupported (see above)
  • r-seabin39 9e9cd0e — pkg / nexe | third-party bundlers embedding a separate, forked Node runtime this project does not control the patch cadence of; pkg is archived upstream. Trades the Node runtime dependency this ticket removes for a different, less-maintained one
  • r-seabin39 9e9cd0e — Deno compile / Bun compile | a different runtime. node:sqlite, the TypeScript, and NodeNext resolution are all Node-specific; retargeting them is a second runtime port, not a build step, and issue B-09 · Single static binary — remove the Node runtime dependency #39's own first option ("Node SEA -- no source rewrite") needs none
  • r-seabin39 9e9cd0e — reimplement in Go/Rust | issue B-09 · Single static binary — remove the Node runtime dependency #39's own second option, and a real one via spec/fixtures + spec/contract-cases, but an order of magnitude more work than this ticket and not needed to solve either problem (latency, no-Node-on-PATH) this ticket opens with
  • r-seabin39 9e9cd0e — committing dist/commitlore next to dist/commitlore.mjs | breaks ADR-0011's committed-dist/-matches-src/ invariant at ~115 MiB per platform/arch, and a pushed blob that size is not removable from git history again
  • r-seabin39 9e9cd0e — Windows (commitlore.exe) in this PR | Node's docs describe a signtool path this repository has no CI runner to verify; shipping an unverified platform claim is what this project's numbers-or-silence discipline exists to refuse. classifyBinTarget and the resolution order are written so it is a small additive follow-up, not a redesign
  • r-distrace88 d118a73 — consolidating the four files' redundant tsc rebuilds into one vitest globalSetup | would remove the race for every file at once and is the more complete root-cause fix, but touches five files' bootstrap plus global config, and mcp.test.ts's beforeAll already tolerates unrelated in-flight tsc errors elsewhere in src/ (OWNED_SOURCES filtering) in a way a single shared build step cannot preserve without its own redesign — bigger blast radius than this issue asked for.
  • r-distrace88 d118a73 — raising bench-ablation's timeout, globally or per-test | proven not to be a timeout problem at all: execFileSync blocks synchronously, so vitest's timeout mechanism cannot even engage against it (verified with a probe test); the actual failure is the runner subprocess legitimately exiting 1.
  • r-parsemulti 6d39d25 — expanding findIdCollisions/index-db to flag a same-message duplicate id generally | reaches into query.ts's lifecycle fold and every consumer route (context, guard, stale) for a case bug-issue-89 only asked parse to surface; the smaller, requested fix is in parse alone.
  • r-multirec01 92aeb24 — minting a fresh Record-Id for an inherited record that never declared one | no other code path in this project invents identity; Record-Id is always author-declared, and doing it here would be new scope beyond what the issue asked for
  • r-multirec01 92aeb24 — a CI step comparing a PR's commits against its post-merge squash commit, as the primary mechanism for finding 1 | needs a GitHub API dependency this tool takes nowhere else, and can only run after the squash is already pushed; doctor catches the same mistake locally, before push, when the source branch is still in refs/heads
  • r-83d43117 8a49ddc — an install-root check for COMMITLORE_BIN | its only reason to exist is aiming the hook at a build outside the install root -- a test harness, a monorepo's local bin. Restricting its location would remove the one thing it is for; doctor's existing COMMITLORE_BIN visibility is the right amount of scrutiny for a channel that is supposed to point anywhere.
  • r-83d43117 8a49ddc — resolving commitlore.bin with an external realpath/readlink -f binary | neither is guaranteed on every machine a hook runs on, the same reasoning 1fd0d53 gave for not shelling out to shasum. cd ... && pwd -P is a POSIX-specified builtin and covers the same physical-resolution need without a new dependency.
  • r-exit065 e545dee — leaving guard's inversion in place and only documenting it | guard and inject are the two commands wired into hooks and CI, so a script written against guard today is already trusting the number; a documented inversion is still an inversion, and it gets load-bearing the longer it sits. The surface is one boolean per command right now — cheaper to fix once than to explain forever.
  • r-fix70a1 d707fc7 — exhaustive per-language phrase enumeration | unbounded phrase lists cannot provide semantic coverage, so this fix documents a bounded lexical policy and independent corpus
  • r-shwt66 5efa206 — checking --git-dir/shallow | linked worktrees keep the shallow marker in the common Git directory
  • r-fix760 fb8ba45 — testing commits with more than one parent in dogfood scope | their platform-generated merge messages carry no authored decision, so requiring a record would require one nobody wrote
  • r-refint74 572f573 — allowing a note to extend a commit record under the same Record-Id | notes are remote-reachable, so divergent content would inherit a human-approved identity
  • r-warn75 24c7cc8 — accepting run-on or indented trailers | Git treats them as prose and accepting them would make records ambiguous
  • r-doctor72 996bcde — choosing either parent doctor list | each drops a runtime check required by the other incident
  • r-fix067 a915af0 — treat an outside-repository file_path as a legitimate no-record result | only an in-repository query can truthfully establish that no records apply; silence would hide a broken matcher
  • r-fix063h f0bb995 — leave hook runtime as a separate contradictory check | a fix-bearing installation check must not report ok or warn when the installed hook demonstrably fails
  • r-fix063 0b8c496 — add the explicit refspec only after confirming the remote ref exists | setup would depend on today’s remote state and require another doctor run after the first notes push
  • r-det058 695cdf6 — add the 100k history suite to CI | CI budget and deterministic local measurement are separate decisions
  • r-fix053 ecc4b90 — add a public mirrored boolean | the per-record sources array already models contributing channels, so a second representation would create drift
  • r-fix055 43b40f8 — infer whether a quote supports Verified | deterministic text matching can prove presence, not that a check ran
  • r-fix055 43b40f8 — downgrade harvested Verified to reconstructed | it preserves a citation-bearing assertion the verifier cannot substantiate
  • r-fix054 664d4e2 — write X-Inherited-From into commit messages too | transport metadata would lengthen every preserved user-facing message merely to restore symmetry
  • r-fix054 664d4e2 — stop squash-preserve from writing notes | it discards the mirror instead of fixing the query seam
  • r-fix056 55cb8bc — reuse the injection omission list for blocked withholding | it includes prose-bearing Evidence and Expires, so it is not a safety boundary
  • r-3b57e2 30f2d5f — converting the three translated READMEs for consistency | they are the product, not the record, and two checks exist specifically to keep them
  • r-7b26f1 ec070ec — retrying the read inside the transaction | a write lock held across a subprocess is a worse failure than the one being fixed
  • r-1e58d3 1fd0d53 — removing the recorded-path branch entirely | a clone is on no PATH and in no node_modules (ADR-0011), and that branch is the only thing that finds the CLI there
  • r-1e58d3 1fd0d53 — hashing the recorded binary at install time | shasum is not guaranteed on every machine a hook runs on, and a check that silently no-ops is worse than the one being replaced
  • r-6f92c4 3cebb89 — restating the withdrawn numbers as prose ("we measured a reduction") | it is the same claim with the evidence removed
  • r-6f92c4 3cebb89 — leading with the protocol's features and putting the measurement record near the bottom | that is the arrangement of someone hoping it is not read
  • r-5e91c7 f991389 — leaving pull_request as the only check for develop | a direct push to an integration branch is exactly what nobody reviews
  • r-2b58d4 4842356 — exempting datasets written before the fields existed | it is one line and it deletes the guarantee
  • r-9c74b3 68340e4 — withholding only in --json | a shell agent reads stdout, and the text form is what it reads
  • r-8e15c2 70f6bcf — hashing only the files the entry point transitively imports | it is the same judgement call that produced this bug, made with more machinery
  • r-4b17f8 7efba5c — retrying the read inside the transaction | a transaction holding a write lock while it shells out to git is a lock held across a subprocess
  • r-7a48c3 b85d847 — a CONFLICTED lifecycle state for divergent declarations | see above — it would block work on records that have a correct answer
  • r-8d51a6 27f73b0 — filtering blocked matches out of the result | the caller needs to know something matched; withholding is a rendering decision, made once
  • r-8d51a6 27f73b0 — reusing exit 1 for "could not check" | 1 already means a broken invocation, and a hook that cannot tell a bad flag from an unreadable repository will treat both as noise
  • r-2f7d94 a7673d0 — an allow-list of free-text keys | it is the shape of the original bug, and a new key would be unguarded until someone remembered
  • r-2f7d94 a7673d0 — leaving the wording generic ("a trailer") | an operator needs to know which line to edit, and the key was available two frames up
  • r-4e29b7 66829bb — folding this into the existing notes field | they are independent axes and can co-occur; one enum would have to enumerate the product
  • r-4e29b7 66829bb — throwing on an unreadable repository | context runs from a hook on every edit, and an exception there is a broken editor rather than a refusal
  • r-6b83f2 4c1a503 — deleting both sentences | the clone-runs-without-installing claim is true for validate, context, guard and the MCP server, and dropping it would understate what a clone gives you as badly as the old text overstated it
  • r-1c47e9 0e9930b — dropping the check | the two installation failures it exists for are real and were both invisible to configuration reads
  • r-1c47e9 0e9930b — probing whichever file is newer | "which artifact is this installation" is a fact about the layout, not about timestamps
  • r-3d92a8 f85101a — keeping the searches first and fixing the shim | the shim belongs to npm, not to us, and the version-skew problem survives the fix
  • r-3d92a8 f85101a — a config-only hook check | it was written, it reported ok, and the hook failed on the next commit
  • r-7c05e2 218ea28 — fetching notes automatically when the ref is missing | a query is a read, and silently reaching the network on a read is a surprise that belongs to git fetch
  • r-7c05e2 218ea28 — leaving it to doctor | doctor is run by a person once, and the answer that misleads is the one an agent gets on every task
  • r-7c05e2 218ea28 — a diagnostic string alone | the field it qualifies is records: [], and prose is not something a consumer can branch on
  • r-9b31c7 e8d45fb — keeping the placeholder until author trust was configurable | the placeholder was the permissive direction, so waiting meant shipping the hole
  • r-9b31c7 e8d45fb — withholding blocked payloads from the CLI too | a person reading a terminal can disbelieve a sentence; a tool result is retrieved fact
  • r-9b31c7 e8d45fb — dropping blocked records from the MCP answer entirely | an agent that silently receives less than there is cannot notice, and cannot audit
  • r-6c48b2 aaadedf — matching the whole file at edit time | the file contains everything the agent did not write, and GUARD-CANNOT-BLOCK measured prose surfaces producing false alarms specifically on compliant agents
  • r-6c48b2 aaadedf — blocking on a match | the score bands overlap, measured
  • r-6c48b2 aaadedf — running T-705 · guard route benchmark — measure the path SPEC §5 assigned to Ruled-out #37 without this pre-check | an arm that fires zero times measures nothing, and 120 runs is an expensive way to learn that
  • r-5b9e37 010782c — baking the resolved path into the hook stub | hooks status is a byte comparison against commitMsgStub(), so every hook installed from a different checkout would report outdated forever
  • r-5b9e37 010782c — an npx fallback | the existing comment is right — npx --no still queries the registry when the package is absent, putting a network call on every commit and breaking offline commits
  • r-2f9c40 07f47ca — wiring guard into the plugin as a blocking hook | true and false positives occupy the same score band on real agent output, so the only precision-safe threshold catches 1 of 5 and every useful threshold blocks four compliant edits in twenty-five
  • r-2f9c40 07f47ca — raising RECORD_ID_WEIGHT's threshold instead of gating the signal | the false alarms scored 1.0000, so no threshold below the maximum excludes them and the maximum excludes everything
  • r-2f9c40 07f47ca — semantic matching to separate the populations | ADR-0002 keeps the core LLM-free and zero-cost, and B-04 · Optional embedding-search tier #31 registers embeddings as opt-in — nothing measured here justifies moving that into the core
  • r-7a3e91 cf859e4 — inlining spec/SPEC.md and the schema into the bundle | SPEC.md would need a codegen step that itself needs a drift guard, and the package-root walk removes the reason to want it
  • r-7a3e91 cf859e4 — replacing the tsc output with the bundle | test/cli.test.ts, test/hooks.test.ts and test/mcp.test.ts import dist internals by path
  • r-c53d19 110be8c — leaving the claim and letting T-706 · Bundle the CLI as a single file — run from a clone alone #38 make it true later | the README is what someone reads while deciding to adopt this, and a claim that is false today does not become honest because it is scheduled
  • r-6f2a08 4c2d432 — forcing the ajv-formats import with a cast to make the bundle build | it swaps a load-time failure for a type lie, at the end of a long session, on the one path every command goes through
  • r-9c07e2 9c4d25a — invoking npx on every Edit | it puts a registry round trip on the hot path of every tool call, which is how a hook earns being uninstalled
  • r-9c07e2 9c4d25a — committing dist/ so the plugin is self-contained from a git clone | it puts build output in review diffs forever to save one background install
  • r-0d4b81 8005227 — a longer quickstart that demonstrates context, limits, ruled-out, warnings and stale | an agent calls those itself once the MCP server is registered, so listing them teaches the human a workflow that is not theirs
  • r-3b8f52 1f8b4be — enabling the large fixture for the whole default suite | generating 100k commits costs ~22s and the other tests in that file gain nothing from it, so it runs as its own step against its own file
  • r-7f31c9 750ab17 — reporting both datasets from one source list | readSources groups by condition and cannot separate repositories, so any second dataset with a commitlore-on arm silently corrupts the headline test
  • r-9c2f74 d653153 — resume the pilot into the same file | a new process would load the edited code and create the mixing that had not happened
  • r-9c2f74 d653153 — run the ablation arms as they stand | three nulls from comparing identical inputs read as "these guarantees do not matter"
  • r-9c2f74 d653153 — keep the tasks that showed an effect and rewrite only the rest | the property is the criterion, not the direction of the result
  • r-4a8e15 49e12c7 — make the parser accept a bare block | it would disagree with git, and delegating to git is the one thing SPEC section 2 requires
  • r-6e1a72 5e09846 — leave the banner until release | it understates for weeks and readers leave rather than build from source
  • r-6e1a72 5e09846 — update English only and translate later | the lag is itself a wrong answer for whoever reads the other three
  • r-0c5d38 aeb54a6 — smoke with --version only | it passes with the schema missing, which is the failure worth catching
  • r-0c5d38 aeb54a6 — assert a file list in package.json | it goes stale silently, and installing is the actual question
  • r-4e9c72 a7a7e26 — fail when the index is missing or stale | it would be the first thing users learn to ignore, and then a real failure is quiet too
  • r-6f2e58 ea9ae6d — pin the adoption sha in the workflow | it is the same stale cutoff the dogfooding suite was written to avoid
  • r-6f2e58 ea9ae6d — spawn git per commit to read trailers | one pass with %(trailers:key=…) uses git's parser and does not scale with history
  • r-7e5f02 e5f5e00 — hold Node 20 and downgrade both dependencies | paying maintenance cost to keep supporting an unpatched runtime
  • r-7e5f02 e5f5e00 — floor at 24 | excludes Node 22 users today for no benefit, and 22 outlives v0.1.0
  • r-7e5f02 e5f5e00 — drop engines entirely | the failure moves from install time to runtime, where it is someone else's confusing bug
  • r-3a9d68 6a3fc3b — pin an expected test count | it goes stale on the next file added, and a stale count reads as passing
  • r-3a9d68 6a3fc3b — keep threads and skip the index tests in CI | that is the same hole with the sign flipped, made deliberate
  • r-9a5e17 6d68703 — let each command edit src/cli.ts | guaranteed conflict, and the conflict surfaces only after every worker has finished
  • r-9a5e17 6d68703 — npx fallback in the hook stub | a network call on every commit, and offline commits start failing
  • r-4d8a13 b1034ca — reject all free text and require a date | conditions are a real use, and the stale engine already flags them for review
  • r-4d8a13 b1034ca — accept 2026-2-15 as a date | the spec names one form, and accepting two invites a third
  • r-8e2d51 ef93c0e — pin one git version in CI | it would hide exactly the divergence the matrix exists to surface
  • r-8e2d51 ef93c0e — allow shallow checkout and skip dogfooding | a skipped gate reports the same green as a passing one
  • r-5a8c04 c46a577 — regex trailer parsing | B3 prose false-positives are the failure mode this protocol exists to prevent
  • r-5a8c04 c46a577 — a custom GitError subclass | the convention here is new Error with structured context, and callers read GitResult
  • r-9d31b7 4ac6e30 — fix the values and move on | the same drift already happened once through a rename, and prose review did not catch it either time
  • r-9d31b7 4ac6e30 — parse the README at runtime in the CLI | the check belongs in the conformance suite, not in shipped code
  • r-c0f4e2 3d249cd — GitLore published as git-lore | the binary and search results still collide with the existing gitlore tool
  • r-c0f4e2 3d249cd — keep Annals | the sound problem does not decay, and with code near zero this is the cheapest moment the project will ever have
  • r-c0f4e2 3d249cd — rename code and spec first, documents later | the drift window makes every artifact written in it wrong
  • r-b2e7f1 00d348d — line-matching Key: prefixes | prose containing a colon line parses as a record and feeds agents false context (verified B3)
  • r-b2e7f1 00d348d — content-hash Record-Id | must survive rebase and squash, which a hash of the commit does not
  • r-a8f3c1 ef48843 — keep name, change vocabulary only | vocabulary is the protocol, so half the change leaves the substance untouched
  • r-a8f3c1 ef48843 — drop Certainty as a dead field | a real route exists -- stale sweep prioritizes guess-level records for review

Warnings (67)

  • r-seabin39 9e9cd0e (claim) — node:sea is "Active development" per Node's own docs; its schema or CommonJS-only constraint could change between Node versions. core/paths.ts's readInstalledFile/isSea split and build-binary.mjs's asset map are the one place that assumption is absorbed, same posture as ADR-0012 already committed to for node:sqlite
  • r-distrace88 d118a73 (claim) — COMMITLORE_BENCH_DIST_DIR is read once at module load (bench/hooks-settings.ts DIST_DIR is a top-level const) — set it before importing the module, not after.
  • r-parsemulti 6d39d25 (claim) — context and validate still do not catch two commit-sourced blocks in the current message sharing one Record-Id unless a notes mirror also disagrees (findIdCollisions requires a notes-sourced record in the group) — parse's check is intentionally independent, not a call into that function, so fixing this in context/validate is separate work.
  • r-multirec01 92aeb24 (claim) — index-db.ts SCHEMA_VERSION is now 2 for the added block column — bump it again, not the shape in place, the next time the trailers table changes.
  • r-83d43117 8a49ddc (claim) — the containment check resolves the recorded path's directory physically and separately rejects a symlink at the final component, but does not protect a relative commitlore.bin with no path separator (a bare filename), which resolves against the hook's cwd and could coincide with the install root in a repository that installs commitlore against itself. That residual case sits in the same accepted-risk category as the severity note above: an attacker who can write that config key already has an equivalent, direct route.
  • r-exit065 e545dee (claim) — guard's swap is a breaking change for anything scripted against the old exit 2 for "matched" — see CHANGELOG Unreleased/Breaking
  • r-fix70a1 d707fc7 (claim) — add malicious and benign fixtures together when extending scanner patterns; false positives can make the defence unusable
  • r-merge66 40e7987 (claim) — test/hooks.test.ts must keep both worktree git-path resolution and reference-integrity assertions
  • r-fix760 fb8ba45 (claim) — unknown-only final paragraphs are treated as prose only when the source is a multi-parent commit
  • r-wt77fix f77ef5d (claim) — git rev-parse --git-path may return a repository-relative path; resolve it against the test repository
  • r-refint74 572f573 (claim) — exact commit and note mirrors remain one logical record; only divergent note payloads collide
  • r-warn75 24c7cc8 (claim) — query several paths one at a time when rename history matters
  • r-shallow66 60a8659 (claim) — shallow history remains advisory; query and guard exit-code semantics are unchanged
  • r-doctor72 996bcde (claim) — keep commit-msg health bound to hook-runtime and keep inject-runtime as a separate known-good payload probe
  • r-fix067 a915af0 (claim) — the accepted tool set is the union of the plugin and settings matchers; aligning those matchers remains Shallow clone: answers from 1 commit of history without saying history is truncated #66
  • r-fix063 0b8c496 (claim) — the wildcard fetches every ref under refs/notes, including notes owned by other tools
  • r-det058 695cdf6 (claim) — guard precision is against the frozen archived labels, not a new hand-adjudication
  • r-fix055 43b40f8 (claim) — Verified remains valid protocol vocabulary for facts recorded from actual command or test execution; only harvest refuses it
  • r-3b57e2 30f2d5f (claim)bench/PREREGISTRATION.md is append-only and was translated in place. Its section numbering and order are unchanged, but a translation is still an edit to a file whose whole discipline is that it is not edited. Recorded here rather than left to be noticed
  • r-7b26f1 ec070ec (claim) — the concurrency test is deterministic rather than sleep-based, so it proves the transaction boundary and not the absence of every race
  • r-1e58d3 1fd0d53 (claim)COMMITLORE_BIN still accepts any executable, deliberately — a harness must be able to aim the hook at a specific build. It is now reported rather than restricted
  • r-6f92c4 3cebb89 (claim) — with no numbers, the first-impression case now rests entirely on the test links. If M3-b also comes back null, this framing is what the project has
  • r-7f24a9 96d960a (claim) — this file is still NOT validated as a GitHub workflow. actionlint is unavailable and the sandbox cannot reach the API. Conflating a YAML parse with workflow validation broke gitseed's CI earlier today; the first run is the proof
  • r-7f24a9 96d960a (claim) — the same task was told to update the factory skill's Gitflow section and its phase-gate.py, and could not — it was launched rooted at this repository and had no write access to ~/.claude/skills. The gate still checks for a branch named develop and fails both repositories for having dev. My spec error, not the delegate's; it is the next task
  • r-5e91c7 f991389 (claim) — these files are NOT validated as GitHub workflows. actionlint is unavailable and the sandbox cannot reach the API, so only the YAML parse and structural assertions ran. Conflating those two broke gitseed's CI four commits ago; the first GitHub run is the proof
  • r-2b58d4 4842356 (claim) — this leaves the README with no measured numbers at all until M3-b runs. That is the honest state and it is also a worse first impression. The alternative was publishing numbers produced by a binary nobody recorded
  • r-9c74b3 68340e4 (claim)context now prints [blocked] beside a record whose payload is gone, which is more visually alarming than the old silent leak. That is the intended direction — a withheld record should be conspicuous — but it changes what a clean repository's output looks like the first time someone commits a Warn: that trips a pattern by accident
  • r-1a63f5 2bb4993 (claim) — "CI is green" was said five times today against a red CI, including in the commit that introduced the rule saying to check CI before saying it. The rule is in docs/RELEASE-GATE.md §5 and it was not followed by its own author. This commit is not claiming CI is green; that claim comes after the run reports
  • r-8e15c2 70f6bcf (claim) — this is the second time in two commits that a fix for the M3 failure did not actually cover the M3 failure. The first was caught by testing the fix against the incident rather than against its own tests
  • r-4b17f8 7efba5c (claim)deleteNoteRows opens its own transaction inside the new outer one. better-sqlite3 nests these as savepoints; node:sqlite has neither, so ADR-0012's migration must flatten this rather than assume it works
  • r-7a48c3 b85d847 (claim) — these two changes were developed concurrently in one worktree and share a built dist/. Splitting them would leave one commit whose dist/ did not match its src/, so they land together and are described together
  • r-5c92e0 73b1285 (claim) — the delegate reported "943 passed" for a suite whose baseline is 1108. It ran while another task was writing to the same worktree and collected a partial set. The real count, verified here on a quiet tree, is 1109 across 31 files — but a delegated test count is now a claim to check, not a result to accept
  • r-8d51a6 27f73b0 (claim) — guard stays advisory. Nothing here makes it block, and GUARD-CANNOT-BLOCK still holds — the point is that it no longer lies about what it saw
  • r-2f7d94 a7673d0 (claim)Evidence: and Expires: are now scanned. Both usually hold paths and dates, so a false positive there withholds a legitimate record. No case is known; a legitimate record carrying a path, a URL and a date was checked and passes
  • r-4e29b7 66829bb (claim)historyAvailability spends two git invocations per query. Both are metadata reads, but this is a hot path and nothing measures it yet
  • r-6b83f2 4c1a503 (claim) — the second claim will become true when ADR-0012 lands and false again if the notes refspec story changes. A README sentence about distribution needs a test, and there is none — scripts/check-readme-numbers.mjs checks numbers
  • r-1c47e9 0e9930b (claim) — this is the second defect in three days from assuming the development checkout is the deployment. The first was exec node in the run script
  • r-3d92a8 f85101a (claim)hook-runtime executes the hook on every doctor run. The probe message is valid so nothing is written, but it is no longer a read-only command
  • r-3d92a8 f85101a (claim) — the check pins PATH to /usr/bin:/bin, which assumes git is there. On a system where it is not, this reports a hook failure that is really a probe failure
  • r-7c05e2 218ea28 (claim)notesAvailability runs git rev-parse and up to two git config reads on every query. Config-only, no network, but it is not free on a hot path
  • r-9b31c7 e8d45fb (claim) — the default is now fail-closed on every route — with no --trusted-author, every Warn: grades claim. That is SPEC §7 and it is what inject already did, but a user who saw [directive] yesterday will see [claim] today
  • r-6c48b2 aaadedf (claim) — recall here is against four re-proposals. It is a go/no-go signal for whether an arm has anything to measure, not an effect size
  • r-5b9e37 010782c (claim)commitlore.bin and commitlore.node are local config, so they do not survive a fresh clone of a repository whose hook was installed elsewhere — re-run hooks install there, which is what the failure message now says
  • r-2f9c40 07f47ca (claim) — --require-content changes precision, not recall; it removes a false-alarm class and catches nothing new
  • r-7a3e91 cf859e4 (claim) — hardcoding ../ counts back to the package root is what broke this — new code reads assets through installedPath(), never through import.meta.url
  • r-c53d19 110be8c (claim) — r-6f2a08's message says the clone gap closes with B-09 · Single static binary — remove the Node runtime dependency #39; it closes with T-706 · Bundle the CLI as a single file — run from a clone alone #38. The commit message is history and stays as written
  • r-6f2a08 4c2d432 (claim) — the READMEs and ADR-0011 currently overstate this — they say a clone runs, and it does not until B-09 · Single static binary — remove the Node runtime dependency #39 lands
  • r-9c07e2 9c4d25a (claim) — hooks here must exit 0 on every path — a non-zero exit from a PreToolUse hook blocks the edit, and no record is worth that
  • r-0d4b81 8005227 (claim)claude mcp add commitlore -- commitlore mcp is Claude Code's syntax — other MCP clients register a stdio server their own way
  • r-7f31c9 750ab17 (claim) — adding a file to README_SOURCES pools it into every aggregate in the block, including the significance test — check the arm names first
  • r-9c2f74 d653153 (claim) — after the measurement, check that git status is clean and the recorded sha is still HEAD -- an edit mid-run breaks reproducibility silently, and that check is the only thing that catches it
  • r-4a8e15 49e12c7 (claim) — canonical output is a block, not a message -- prepend a subject before parsing it back, or the round-trip silently returns nothing
  • r-6e1a72 5e09846 (claim) — the moment v0.1.0 is published this banner is wrong again -- it names npx not working, which release makes false
  • r-0c5d38 aeb54a6 (claim) — spec/ ships on purpose -- alternative implementations are told the fixtures are the contract, and this step is what keeps that true
  • r-4e9c72 a7a7e26 (claim) — doctor reads the index read-only and never rebuilds it -- a diagnostic that repairs on sight hides how often the repair was needed
  • r-7e5f02 e5f5e00 (claim) — raising the floor again needs an ADR superseding ADR-0010 -- a package.json-only change will pass tests and lie to users
  • r-3a9d68 6a3fc3b (claim) — any native addon added later needs the fork pool -- moving back to threads will silently drop whatever loads it
  • r-9a5e17 6d68703 (claim) — commands are advertised in --help only once they work -- test/cli.test.ts holds the landed and unlanded lists, and moving a name between them belongs in the commit that wires it
  • r-4d8a13 b1034ca (claim) — the two Expires branches must stay disjoint -- widening the free-text branch without narrowing the date-shape exclusion puts the typo hole straight back
  • r-8e2d51 ef93c0e (claim) — keep fetch-depth 0 on any job that runs npm test -- the dogfooding suite fails deliberately on a shallow clone, and "fixing" that by relaxing the assertion removes the gate
  • r-7f0e39 76f3f2d (claim) — when retiring a command name, grep the bare word too, not just the prefixed form -- the prefix is what the substitution keyed on
  • r-5a8c04 c46a577 (claim) — do not add --grep anywhere under src/ -- test/source-guards enforces its absence and will fail the build
  • r-9d31b7 4ac6e30 (claim) — the four READMEs must keep the example block byte-identical -- translating the code block will fail spec/verify.sh
  • r-c0f4e2 3d249cd (claim) — ADR-0008 and ADR-0009 keep the literal string Annals on purpose -- mechanical substitution there destroys the decision trail
  • r-c0f4e2 3d249cd (claim) — the residual grep for lore_query reports a false positive because commitlore_query contains it as a substring, so check the prefix
  • r-b2e7f1 00d348d (claim) — spec/fixtures and spec/contract-cases are the contract -- prose in SPEC.md explains them, it does not define them
  • r-a8f3c1 ef48843 (claim) — docs/adr/ADR-0008 is the canonical vocabulary -- do not reintroduce old terms from memory

git log --follow accepts exactly one pathspec, so renames are not followed for 49 paths; query one path at a time to follow its rename chain

withheld the content of 3 record(s) graded blocked: a Ruled-out, Verified trailers matching an injection pattern is reported, never quoted (SPEC §7)

Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR.

…store provenance (#39)

#39's own AC named this the last piece: "attach platform binaries to the
release." `.github/workflows/release.yml` builds it on top of PR #94's
compiled-binary work without touching #94's own decision (the binary stays
uncommitted; CI still builds and smoke-tests it on every push) — this adds
the second, separate act of publishing one when an owner tags a version.

Trigger is `on: push: tags: ["v*"]` only — no `schedule`, no `branches`, no
`workflow_dispatch`. Cutting a release is an owner action; pushing the tag
is the only approval this workflow recognizes, and it has no path to fire on
its own.

Three jobs, matching the shape ripgrep/fd/bat/gh/deno/bun all ship rather
than inventing one:

- `version-consistency` (new `scripts/check-release-version.mjs`) runs
  first and gates everything else: the tag, package.json's `version`, and
  `commitlore --version` must all agree, or the release stops before a
  single binary is built. A tag is immutable once fetched and an asset is
  immutable once downloaded, so this is the only point where a mismatch is
  still cheap to catch.
- `build` matrix — macos-latest (arm64), macos-13 (x64), ubuntu-latest
  (x64), ubuntu-24.04-arm (arm64) — packages each as
  `commitlore-<version>-<target-triple>.tar.gz`, the Rust-ecosystem naming
  convention install scripts already expect to parse, and attests each
  asset with `actions/attest-build-provenance` (Sigstore-backed, keyless,
  no key this project manages — and the harder-than-usual case for a
  project whose own README argues decision records must be verifiable).
  `scripts/build-binary.mjs` always injects into the *running* node binary
  (its own comments), so a native runner per target is not a convenience
  here, it is the only way this build works — there is no cross-target
  flag to give it.
- `publish` downloads every matrix artifact, computes one `SHA256SUMS`
  covering all four assets together (a per-platform file would let one
  asset go unverifiable without the download even failing), attests the
  checksum file too, and creates the release with `gh release create
  --generate-notes`.

Linux arm64 needed no emulation gymnastics: GitHub's own `ubuntu-24.04-arm`
hosted runner is available to this (public) repository, confirmed by
actually building and running the SEA binary on it, not assumed — see
Verified.

Ruled-out: windows-latest | AC asked for it, and the task was to verify
  before assuming, not to skip verifying. Two real, reproducible defects
  found on a real windows-latest run of this exact branch, not merely an
  absent runner (see Verified for the throwaway probe): (1)
  `scripts/build-binary.mjs`'s `REPO_ROOT` is computed as `resolve(new
  URL('..', import.meta.url).pathname)`; a Windows file:// URL's
  `.pathname` keeps its leading slash in front of the drive letter, and
  win32 `path.resolve` then doubles it (`D:\D:\a\commitlore\...`) —
  `npm run build:binary` fails before SEA is even reached. Not a one-off:
  `scripts/check-engines.mjs` has the identical pattern, so it is a
  systemic gap in the script/ layer. (2) Even past that,
  `classifyBinTarget` (`src/core/hook-target.ts`) and the `commit-msg`
  shell stub's `case` patterns recognize a compiled binary only by the
  exact basename `commitlore` — never `commitlore.exe`, which is what
  `build-binary.mjs`'s own `OUTPUT` already names the Windows build. The
  installed hook would silently fall through #71's containment check
  rather than refuse to resolve it, on the one platform that most needs
  that check to hold. ADR-0015 already named this "a small, additive
  follow-up" — confirmed here empirically, not fixed here: fixing it is a
  source change to #94's own shipped hook-target logic, a different scope
  than building the pipeline that ships what already works. Windows stays
  out of the matrix until both are fixed and re-verified on a real runner.
Warn: `node scripts/check-release-version.mjs` only checks the script
  build's `--version` (`dist/commitlore.mjs`), not the compiled binary's —
  correct today because both read the identical `packageVersion()`
  (`src/core/paths.ts`) against the identical `package.json`, but that
  argument would need re-checking if the two build paths ever diverge.
Limit: the whole tag-triggered flow (three jobs, the real `gh release
  create`, the real attestation publish step) has never run end to end —
  cutting a release is explicitly the owner's action, not this change's.
  What was verified below is every component this repository allows
  verifying without that: the matrix runners for real (via a throwaway,
  deleted probe branch, not this workflow), the version-consistency script
  against real passing and deliberately-mismatched input, and the workflow
  file's own YAML validity.
Verified: `.github/workflows/release.yml` parses as valid YAML
  (`python3 -c "import yaml; yaml.safe_load(...)"`); no `act`/`actionlint`
  available in this environment, so job-graph/expression-level linting is
  unexercised
Verified: `node scripts/check-release-version.mjs v0.1.0` against this
  tree's real package.json (0.1.0) and real `dist/commitlore.mjs
  --version` (0.1.0) — passes, exit 0. `v9.9.9` against the same tree —
  fails with both disagreements listed, exit 1. `not-a-tag` — rejected as
  not looking like a version, exit 2. `GITHUB_REF_NAME=v0.1.0` env-var path
  — same pass as the argument path
Verified: ubuntu-24.04-arm is a real, non-emulated GitHub-hosted runner for
  this repository — built dist/commitlore.mjs and dist/commitlore on it via
  a throwaway workflow_dispatch/push-triggered probe branch
  (`probe/windows-sea`, deleted after), `uname -m` reported `aarch64`,
  `npm run build:binary` succeeded, `./dist/commitlore --version` and
  `doctor` both ran clean
Verified: the same probe, on windows-latest, reproduced the `REPO_ROOT`
  doubling failure described above verbatim
  (`D:\D:\a\commitlore\commitlore\dist\commitlore.mjs does not exist`) —
  not inferred from reading the code alone, though the code reading (item
  2 above) independently confirms `classifyBinTarget` would also reject
  `commitlore.exe` had the build itself succeeded
Verified: `npm run typecheck`, `bash spec/verify.sh`, and
  `node scripts/check-readme-numbers.mjs` all still pass against this tree
Blast: system
Undo: easy
Certainty: tentative
Provenance: authored
Evidence: .github/workflows/release.yml
Evidence: scripts/check-release-version.mjs
Evidence: scripts/build-binary.mjs
Evidence: scripts/check-engines.mjs
Evidence: src/core/hook-target.ts
Evidence: src/hooks/commit-msg.ts
Evidence: docs/adr/ADR-0015-single-executable-binary.md
Follows: r-seabin39
Record-Id: r-relworkflow
CommitLore-Version: 2.0.0
…rivate:true gap (#39)

`install.sh` is the second documented install path #37/#39's release
pipeline needed: `curl -fsSL .../install.sh | sh` for a machine with no
Node, no clone, and no interest in either — while never being the *only*
documented path, since piping to a shell should not have to be trusted
blind. Detects OS/arch, maps to the target triple `release.yml` names
assets with, downloads `SHA256SUMS` from the same release first (fixed
filename, no version guessing, no GitHub API call so no rate limit),
resolves this platform's asset out of it, and verifies the checksum
*before* extracting or installing anything. A script that skips that check
is worse than none — it teaches the habit this project's own trust model
argues against.

Honors `$PREFIX` (installs to `$PREFIX/bin`) or `$COMMITLORE_INSTALL_DIR`
(exact destination), defaulting to `$HOME/.local/bin`. Refuses to
overwrite anything it did not install: an existing file at the destination
is only treated as a legitimate upgrade target if running it actually
produces output shaped like this CLI's own `--version` (a bare semver) —
not merely "runs and exits 0", which almost anything does for an
unrecognized flag. Caught during testing, not assumed correct: an earlier
version of this check trusted exit code 0 alone and silently overwrote a
throwaway foreign script that printed unrelated text and exited clean —
see Verified. Unsupported OS/arch (anything but macOS/Linux,
aarch64/x86_64 — no Windows asset yet, ADR-0015) fails loudly with a
pointer to installing from source, never guesses.

All four READMEs get the same two additions, translated rather than
English-only: the curl one-liner plus the equivalent manual
download-verify-extract steps, so the shell-pipe path is never the only
documented one in any language this project ships docs for. While there,
backfilled a gap PR #94 left in the three non-English READMEs: it added
"### Run as a compiled binary, without Node installed" only to README.md,
so README.ko.md/README.ja.md/README.zh-CN.md were both missing that
section entirely and still listed issue #39 under "not done yet" after it
was. Both fixed together since the new release-install section reads as
non-sequitur without the binary section it follows in English.

package.json gets `"private": true`, per issue #93's own note that this is
"worth a single cleanup once #39 lands rather than twice" — #39 has now
landed both halves (the binary, and the pipeline that ships it), and
nothing about this release model works if `npm publish` still succeeds.
`bin`/`files`/runtime `dependencies` are #93's other findings and are
intentionally untouched here — `private: true` is the one that is a direct
prerequisite of "do not re-add npm publishing," the rest is a separate
cleanup with its own blast radius.

Ruled-out: guessing the current version to build the asset URL directly |
  would need either the GitHub API (rate-limited, needs no-auth headers
  handled correctly) or trusting a redirect's final Location header
  parsing. Downloading the fixed-URL SHA256SUMS first and reading the real
  asset name back out of it needs neither and is what the checksum step
  has to fetch anyway.
Ruled-out: `local` for scoping — not POSIX per se, but supported by dash,
  bash, and every shell this script is realistically piped into (verified
  directly, see Verified) | not used in the end; the script has few enough
  variables that scoping was not needed, only noted here because it was
  considered.
Limit: 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.
Verified: full happy path against a real `npm run build:binary` output
  packaged exactly as `release.yml` would (`commitlore-0.1.0-aarch64-apple-
  darwin.tar.gz` + hand-made `SHA256SUMS`, served over a local
  `python3 -m http.server`) — checksum verified, installed, `--version`
  reports 0.1.0. Re-run against the same install: recognized as its own
  prior install, upgraded, not refused
Verified: checksum mismatch (SHA256SUMS hand-edited to a wrong hash) —
  refused before installing anything, exit 3, nothing written to the
  destination directory
Verified: pre-bug — a foreign executable at the destination that prints
  unrelated text and exits 0 on `--version` was silently overwritten by
  the exit-code-only check. Fixed to check the output shape; re-run with
  the identical foreign file — correctly refused, exit 4, foreign file
  untouched (content diffed byte-identical after the refusal)
Verified: unsupported OS (faked `uname -s` → `SunOS`) and unsupported arch
  (`uname -m` → `riscv64`) both fail loudly, exit 1, before any network
  call
Verified: `sh -n` and `dash -n` (strict POSIX) both accept the script with
  no syntax errors; the full happy path re-run under `dash` directly (not
  only bash-as-sh) with identical results
Verified: the manual (non-piped) verify sequence documented in all four
  READMEs — `curl -fsSLO`, `grep ... | shasum -a 256 -c -`, and the Linux
  equivalent `sha256sum -c -` — both produce "OK" against the same
  hand-made SHA256SUMS and locally built asset
Verified: `node scripts/check-readme-numbers.mjs` and `bash spec/verify.sh`
  still pass against all four edited READMEs
Blast: system
Undo: easy
Certainty: firm
Provenance: authored
Evidence: install.sh
Evidence: README.md
Evidence: README.ko.md
Evidence: README.ja.md
Evidence: README.zh-CN.md
Evidence: package.json
Follows: r-relworkflow
Record-Id: r-relinstall
CommitLore-Version: 2.0.0
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