Skip to content

Releases: MongLong0214/commitlore

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 04:40
Immutable release. Only release title and notes can be modified.
90a8b21

Recorded as held for want of a reviewer, not as reviewed. Every change in
this release passed the gate — 3,359 tests, the canonical artifact check, and
CI at the released commit — and none of it had a cross-provider review. The
reviewers this project uses were out of quota on the day it shipped, and
Claude reviewing Claude is not a second opinion: the same model family shares
its blind spots. Two of today's conclusions were reversed by an outside
reviewer earlier in the day, so this is a real gap and not a formality.

The release that makes releases discoverable, and three places where the tool
was doing work nobody read.

commitlore upgrade. There was no way to find out that a newer CommitLore
existed. --version reported what was running and nothing compared it to
anything, so a repository initialised on a stale install kept validating every
commit with a stale protocol and nothing said so. upgrade --check reports the
installed and newest release; the bare form performs the upgrade by invoking the
installer and then reading the link back to confirm it points at the tag that
was asked for -- not merely that it moved, which an installer that moved it
somewhere wrong would satisfy. --json is the scriptable form and answers
inside CI on purpose. A passive notice appears at most once a day and is silent
for hooks, --json, non-terminals, and any command that failed; doctor
carries staleness as a finding, because the notice is silent for --json and
that is the one output built for programs to read. init names the version it
pinned and does not move current; init --upgrade does.

Both installers register the same hook, and only one of them. The CLI wrote
Read|Edit|Write and the plugin shipped Edit|Write|MultiEdit|NotebookEdit, so
a CLI install delivered nothing when the agent edited with MultiEdit and a
plugin install delivered nothing when it read a file before deciding. A matcher
of only letters and | is a list of exact strings rather than a regular
expression, so Edit never covered MultiEdit. Both now register all five.
And init no longer writes its hook when the Claude Code plugin already covers
the repository -- before this, following the README to the plugin and then
running init answered every matched tool call twice.

A hook fire costs a third less, and a rebuild an order of magnitude. Every
fire ran PRAGMA quick_check over the whole index -- 62 ms on a 15 MB one,
before an answer that usually has nothing to deliver. Nothing promises it,
SQLite documents that it does not catch the desynchronised-index case that
would actually hurt, and across 85 corruption trials it never prevented a wrong
answer. It runs on rebuild and in doctor now, and a read that meets corruption
falls back to a scan rather than failing open to silence. Separately, a rebuild
started 4,345 git subprocesses, 4,329 of them parsing trailer paragraphs whose
results were then discarded: 45 s to 2.3 s on this repository, and a
10,000-commit repository from over two minutes to 2.2 s.

Fixed: a partial index answered with silence, which reads as "no records
here" (#778); a rebuild could not rebuild past a schema change (#779) and could
not open a structurally damaged index at all (#785).


Upgrading

curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.0/install.sh | sh -s v1.2.0

An install from 1.2.0 onward tells you when a newer release exists. Earlier
installs cannot, which is what this release is for.

If you have both the Claude Code plugin and a commitlore init hook, the
init hook is now redundant and this release stops writing a new one — but it
does not remove one already on disk. Until you delete it, every matched tool
call is answered twice. commitlore doctor names the state.

Closed in this release

#742 · #775 · #776 · #778 · #779 · #781 · #782 · #785

v1.1.4

Choose a tag to compare

@github-actions github-actions released this 18 Aug 14:16
Immutable release. Only release title and notes can be modified.
533dfd7

One line changed for anyone using the tool. The rest is the repository telling the truth about itself, which is most of what this release is.

The user-visible change

Pushing a tag printed this:

commitlore: notes mirror (origin) failed: spawnSync git ETIMEDOUT. The branch was
pushed; the records for these commits are still only local. …

Nothing was wrong with the transport, and the rest of the line already said what to do. But naming the call that returned rather than the decision that was made reads as git having failed, and sends whoever gets it to look at a remote that is fine.

The two seconds are this hook's — a notes mirror is auxiliary to a branch push, and declining to wait is the hook working. Now:

commitlore: notes mirror (origin) failed: the 2s this hook waits for the remote ran
out. The branch was pushed; …

The value is interpolated from the constant, so the sentence cannot drift from what it describes. Every other failure detail is untouched: a refused connection still reports as a refused connection, because that one really is about the transport.

A contributor no longer needs a Linux Docker build

dist/ is committed, because a first-class install is a git clone. The cost fell on contributors: every pull request touching source had to carry a rebuilt bundle from docker linux/amd64, and someone whose machine cannot produce one waited on a maintainer twice in a single pull request.

A source-only pull request now becomes a product commit without anyone rebuilding by hand. The canonical workflow merges the source onto main, runs the canonical build, and opens a second pull request carrying the result — so every required check runs on the tree that lands, not one resembling it.

Observed rather than argued: a source-only pull request went in, the rebuilt commit came out matching its own source with the bundle authored by the build App, and a hand-tampered bundle was refused by the same path.

The job holding the credential checks what it was handed

The rebuild executes a contributor's package.json and every lifecycle script npm ci pulls in. The publishing job now recomputes the merge itself from main and the pull request ref, pins both parents, and allows a difference only inside dist/ and the manifest. It refuses a pull request that moved while the rebuild ran, reads the pushed branch back by sha rather than trusting the push's exit code, and mints a token scoped to this repository with two permissions instead of whatever the installation happens to hold.

The canonical builder is pinned by digest. node:24-bookworm is a mutable tag, so building twice in one job proved the builder deterministic that morning and nothing about next month.

Documents that were wrong

  • The readiness statement named a release two versions old, said no feature work was planned on a day feature work shipped, and listed a defect as current that had been fixed and closed. It now owns the product contract and nothing that moves on its own.
  • The upgrade contract was two generations and is three. A hook installed between v1.0.2 and v1.1.2 follows current and still refuses the commit under the PATH git gives a hook — which the compatibility page denied. All four READMEs now carry the same table.
  • Three translated READMEs said nothing about upgrading at all, in the release whose predecessor's first line was an upgrade instruction.
  • The generated benchmark block lived in four files and was compared in one. It has a single owner now, and a README that grows a second copy fails.

Compatibility

Nothing here changes what a record is, how one is validated, or what any command outputs on success.

If this repository was set up before v1.1.3, run commitlore hooks install in it once — that is unchanged from the previous release and still true.

Install

curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.1.4/install.sh | sh -s v1.1.4

Full notes: CHANGELOG.md

v1.1.3

Choose a tag to compare

@github-actions github-actions released this 18 Aug 05:38
Immutable release. Only release title and notes can be modified.
e3ee9e8

Run commitlore hooks install once in every repository you have already set up. After that, upgrading stops blocking commits.

That instruction is the release, and it is not rhetorical. The fix below lives in the hook file, and a hook file is written when it is installed — so upgrading to 1.1.3 does not rewrite the hooks already on disk, and the upgrade that delivers this fix is not itself fixed by it.

What was broken

hooks install records two values: commitlore.bin through <data-root>/current, so a hook follows upgrades, and commitlore.root as the physical v<x> that path resolved to at the time. An upgrade moves current and leaves root behind, and the commit-msg hook compares them — so from 1.0.2 onward, every repository wired before an upgrade stopped using its recorded interpreter afterwards.

Under a normal shell that went unnoticed: the hook falls through to a PATH lookup and commits succeed. Under the PATH git actually gives a hook — a GUI client, an IDE, anything launched outside a login shell — there is no commitlore on PATH, and the commit was refused.

What changed

The hook tells an upgrade from a repointed path. hooks install writes commitlore.bin as a literal string ending /current/dist/commitlore.mjs, and an upgrade moves an installer-owned symlink to a sibling tree without changing that string. A .git/config edit — the case commitlore.root exists to refuse — replaces the string itself, and can write neither the installer's symlink nor a directory beside its versioned trees. So the upgrade shape rebinds the trusted root to what current resolves to now and re-runs the same containment check. Everything else is refused exactly as before.

The refusal says what happened. It used to print cannot find the CLI this hook was installed with for three different outcomes: nothing resolved, the recorded path resolved and was refused, and the recorded install no longer exists on disk. The first sentence was false for the other two, and it sent operators looking for a missing file that was present and working. Each now has its own message naming both recorded paths.

hooks install reports the repair it performs. It compared only commitlore.bin, which does not change across an upgrade — so the one command that fixes this printed unchanged after fixing it.

Separately, commitlore init no longer reports that a step needs attention because of a process on the machine. doctor warns when more than one CommitLore MCP server is answering, which is ordinary on a developer machine because <data-root> keeps previous versions; init was treating that as work left undone in the repository it had just set up.

Compatibility

Nothing here changes what a record is, how one is validated, or what any command outputs on success. Nothing in this release reaches a repository on its own.

Install

curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.1.3/install.sh | sh -s v1.1.3

Full notes: CHANGELOG.md

v1.1.2

Choose a tag to compare

@github-actions github-actions released this 17 Aug 12:57
Immutable release. Only release title and notes can be modified.
ad6fee3

Canonical source: ad6fee3; Canonical dist/commitlore.mjs SHA-256: 96141692eaeff0d8a711fe5e54c2d26dadf9c96045847db2d8e65ed245cee62f

Upgrading no longer reports a version it did not install.

If you upgraded to 1.1.0 or 1.1.1, check this first

install.sh printed current -> v1.1.1 and left <data-root>/current pointing at the previous release. commitlore init records the hook's interpreter as <data-root>/current/dist/commitlore.mjs precisely so hooks follow upgrades — so every repository on that machine kept validating commits with the old build while the CLI reported the new one.

readlink ~/.local/share/commitlore/current

If that names an older version than commitlore --version reports, you are affected. Re-running the installer at 1.1.2 repairs it, and commitlore doctor reports the same split in its own words:

warn  commit-msg hook — commitlore.bin is version 1.0.2, but this CLI is 1.1.1 —
      the hook validates every commit with 1.0.2, so anything fixed since then does not apply here

Nothing was lost and no record was corrupted. What was wrong is which build was checking them.

The cause

BSD mv follows a destination that is a symlink to a directory and moves the source into it, returning zero. The && held, the success line printed, and the temporary link sat inside the old release directory where the rename had actually gone:

~/.local/share/commitlore/current                                 -> v1.0.2   (untouched)
~/.local/share/commitlore/v1.0.2/current.commitlore-install.53786 -> v1.1.1   (where mv put it)

A first install creates the link and cannot reach this, which is why it shipped in 1.0.2 and survived two releases. Reproducing it needs two installs, and the tests now do that.

The fix

The rename says it means rename — -h on BSD, -T on GNU, falling back to unlink-and-rename, which is not atomic but fails visibly rather than silently keeping the old build.

And the success line is printed only after reading the link back. That is the half that matters: every mechanism available here can return zero without moving anything, and that line is the only thing an operator reads before trusting an upgrade.

Also in this release

Windows CI exercises the host-detection branches that only a comment was holding. A GitHub runner has no coding agents, so those branches never ran there. A planted claude.cmd and codex.cmd make them run, and a leftover .claude.json with no executable is now required to stay notDetected — the rule 1.1.1 documented and nothing enforced.

The README lines saying host wiring needs 1.1.1 or later do not move with this release. They name the version that fixed it.


Full notes: CHANGELOG.md. Upgrading from 1.1.x is a drop-in; nothing about capture, policy or records changed.

v1.1.1

Choose a tag to compare

@github-actions github-actions released this 17 Aug 06:35
Immutable release. Only release title and notes can be modified.
8c29f5d

Canonical source: 8c29f5d; Canonical dist/commitlore.mjs SHA-256: 96141692eaeff0d8a711fe5e54c2d26dadf9c96045847db2d8e65ed245cee62f

1.1.0 said plainly that host wiring did not work on Windows. Every host that failed for a defect in this project now wires on a real machine: Codex, Gemini CLI and Hermes.

What was fixed

A command could not be found, and could not be run.

hasCommand joined each PATH entry with the bare command name and stopped there, never consulting PATHEXT. Windows installs cursor.cmd, codex.cmd, claude.cmd — there is no extensionless file — so detection was false for every host whose executable is a shim, and claude-code reported notDetected with its config sitting on disk. Separately, spawnSync ran with shell: false, which cannot execute a .cmd shim at all; that produced codex mcp add failed.

Resolution now finds one concrete executable through PATHEXT, and both detection and execution use it. The original defect was those two disagreeing about what "present" means; they are now the same function and can no longer differ. A batch shim is invoked through an explicit cmd.exe argument vector with shell: false preserved — the wrapper path and user config paths reach these calls, and a shell would make quoting an attack surface.

What is still reported failed

ok is still false on that machine, and it should be.

  • .cursor/mcp.json was zero bytes. That is a user file, not a defect here — the installer read the file it says it reads and reported the true reason, since Unexpected end of JSON input is what parsing an empty string produces. It now names the file in that message, so the next reader does not have to establish which of Cursor's config locations was meant.
  • windsurf, opencode and claude-code were notDetected. The first two were genuinely absent. claude-code had a config file but no executable, and detection for it is executable-based; whether that asymmetry is right is open (#728).

Observed

Windows 10.0.19045.0, PowerShell 5.1.19041.6456, with agents installed:

codex        custom-preserved  healthy    plugin installed
gemini-cli   custom-preserved  healthy
hermes       installed         healthy    Hermes setup verified
cursor       failed            C:\Users\u\.cursor\mcp.json is not parseable JSON

Registrations verified through a live MCP Initialize and read back from the configs on disk.

CI cannot establish any of that: a GitHub runner has no coding agents, so every host short-circuits at detection and the Windows job prints "hosts":[] on every run. #722 is the plan to stop that being true.

Two repairs that came from review rather than from the machine

A trailing backslash — which every Windows directory path can carry — reached the cmd.exe argument vector unescaped. The closing quote consumes it and the following argument is absorbed, so a --verify passed that way would vanish and the step would report verified for a verification that never ran. That is the exact failure this project exists to remove, arriving inside the fix for it — and it was not hypothetical: --data-root ends with a backslash on Windows, so this was the argument that broke, and repairing it is what made Hermes work.

Executable resolution now checks X_OK. Without it, a non-executable file of the same name earlier on PATH is selected and spawned — a regression on macOS and Linux, where host wiring already worked. It is guarded by a test that places a mode-0644 shadow ahead of a mode-0755 CLI and runs on POSIX, because every fixture in that suite had been executable and nothing could have failed on it.

A failed host now says why

The Hermes step ran with stdio: 'ignore', so whatever it printed about its own failure was discarded and the report was a bare Hermes setup failed. The reason now reaches the summary. That mattered while Hermes was still failing; the trailing-backslash repair turned out to be its cause, so the message it would have carried is one nobody has to read — but the next failure of any host arrives with its reason attached instead of requiring another trip to the machine.

A literal % in a path is legal on Windows and is handled rather than refused; the reason %% escaping is not the answer is recorded in the code beside it.


Full notes: CHANGELOG.md. Upgrade from 1.1.0 is a drop-in; nothing about capture, policy or records changed.

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 04:20
Immutable release. Only release title and notes can be modified.
d9a041f

Canonical source: d9a041f; Canonical dist/commitlore.mjs SHA-256: 50ad329497c15e9ff8cfd3de9a96470688b41930c21c4f0de83119dc502efcfe

One machine can now differ from the committed capture policy without modifying it.

A local policy overlay (#709)

.commitlore-policy.json is committed with the repository, which is right for a policy: commitlore auto on authorises an agent host to prepare, verify and stage records with nobody in the loop, and that applies to everyone who clones. A contributor who needed a different answer had exactly one route — edit the tracked file — and their worktree then stayed modified forever. That is enough to stop a release script which refuses to tag a dirty tree, which is the failure that was reported.

.commitlore-policy.local.json     per key, wins
.commitlore-policy.json           repository default
built-in defaults

Per key, not per file: an overlay setting only unattended leaves mode and max_records_per_commit as the repository set them, so a later change to the committed file still applies. commitlore auto on --local and auto off --local write it; once it exists it is the file commitlore auto writes, so opting in or out never touches the tracked file again. Keep it out of version control — nothing writes a .gitignore entry for you.

Two things keep the precedence answerable rather than ambiguous. The policy identity hash is computed over the effective policy whenever an overlay is present, so a record prepared under one is stamped with the policy that produced it rather than with bytes that did not — and a repository with no overlay keeps exactly the digest it had, so no capture in flight is refused by this upgrade. And a new policy-overlay doctor check names both files, the value beneath, the value in the overlay, and the one in force.

Reasoning and what it is conditioned on: ADR-0035.

Windows: host wiring still does not work

Read this before upgrading on Windows.

An owner ran 1.0.2 on a real Windows machine with coding agents installed. Every detected host failed and nothing was written to any config. This release fixes one of the two causes, not both.

Still broken (#716):

  • hasCommand joins each PATH entry with the bare command name and never consults PATHEXT, so it cannot see cursor.cmd, codex.cmd or claude.cmd. A host whose executable is not found is reported notDetected even with its config file present.
  • spawnSync runs with shell: false, which cannot execute a .cmd shim. This is what produced codex mcp add failed and Hermes setup failed.

Fixing that needs a design which preserves the reason shell: false was chosen — the wrapper path and user config paths reach those calls — and it cannot be verified anywhere except a Windows machine with agents installed. CI has no coding agents, so its install-ps1 job detects zero hosts and never reaches the code (#714).

What this release does fix on Windows: both atomic writers built the temporary file name out of the whole target path, because path.split('/').pop() returns its argument unchanged when there is no / in it. C:\Users\u\.gemini\settings.json became …\.gemini\.C:\Users\u\.gemini\settings.json.commitlore-….tmp, which cannot exist — so every host that reached its write failed with ENOENT. That is repaired, and pinned by a test a POSIX runner can fail on, which is the only kind of guard this repository can hold against a Windows-only defect.

The installer was honest throughout: ok:false, per-host outcome:"failed", and no config silently changed. It reported failure correctly; it could not do the work.

Every host failure now names the file it was about (#716)

The 1.0.2 run reported cursor as config is not parseable JSON: Unexpected end of JSON input while an independent check of that machine found valid, byte-identical JSON. Both can be true — this command reads %USERPROFILE%\.cursor\mcp.json and only that, while Cursor keeps configuration in more than one place — and nothing in the output could separate them. The path had never been recorded anywhere, so no transcript could have contained it either.

All seven read and write failure paths now lead with the path: <path> is not parseable JSON: …, <path> could not be written atomically: …. The summary schema is unchanged.

doctor no longer claims a record was lost when none was staged (#710)

A repository whose captures never reached staging was told a decision had been "never written to the history". Nothing was dropped there: the commit each draft was prepared for either never happened, or happened without it.

The installers carry only the code that runs (#691)

install.sh and install.ps1 have delegated every detection, config write and MCP probe to one shared TypeScript command since 0.9.0. Both files still carried the superseded shell and PowerShell implementations below an unconditional exit — 845 lines no install has executed, and which three readers in three days took for live code. They are gone; four guards that were asserting against those copies now target the enumeration that runs.


Full notes: CHANGELOG.md.

v1.0.2

Choose a tag to compare

@github-actions github-actions released this 16 Aug 11:55
Immutable release. Only release title and notes can be modified.
3843ed0

Canonical source: 3843ed0; Canonical dist/commitlore.mjs SHA-256: e00f61d84caa991de10c480304820b7c139b183cb37b3e82e2a823296a010574

What's Changed

Full Changelog: v1.0.1...v1.0.2

v1.0.1

Choose a tag to compare

@github-actions github-actions released this 15 Aug 13:45
Immutable release. Only release title and notes can be modified.
b65e34f

Canonical source: b65e34f; Canonical dist/commitlore.mjs SHA-256: 136348243607bca3c8f3130825d458a862444c440307d396f1977b34ea6da910

What's Changed

Full Changelog: v1.0.0...v1.0.1

CommitLore v1.0.0

Choose a tag to compare

@MongLong0214 MongLong0214 released this 15 Aug 08:23
Immutable release. Only release title and notes can be modified.
47359a1

CommitLore v1.0.0

Every behaviour change below was verified present in the code on main, not taken from a plan.


If you are already running CommitLore

Five things behave differently. Two of them change JSON a client reads.

A timeout is no longer a verdict. The MCP probe's budget moved from five
seconds to fifteen, because a passing probe on Windows consumed 4478ms of the
old one. initialize-timed-out now reports could not verify this time rather
than this registration is broken. A closed input, an early exit, or a foreign
identity still say unhealthy.

A cached signature verdict is bound to the keyring that produced it. In
signature mode the index records which keys its verifier could see. A reader
whose keys differ rebuilds instead of serving a verdict about a keyring that is
gone.

Every answer names the build that produced it. runtime.version and
runtime.build_id — a digest of the bundle, not a path. Four generations of
this product were found installed at once, three of them reporting the same
version, so a version alone could not tell them apart. This adds two fields to
the JSON contract.

A partial answer says it is partial. coverage is complete or partial.
The consumer routes stop scanning after three seconds so a large repository
costs a pause rather than four minutes; until now the shorter answer looked
exactly like a whole one, and history and notes both reported healthy
because the sources were healthy. This adds one field to the JSON contract.

No commit can add an agent session identifier to this repository's history.
Checked against a baseline of the three already published, matched on the value's
shape rather than a list of vendor key names.

Upgrading

The three added JSON fields are additive. A client that ignores unknown fields is
unaffected; a client that pins an exact key set will see three it does not know.


If this is the first thing you have read

CommitLore keeps the decisions behind your code in git itself — commit trailers
and notes — so an agent editing a path is handed what the repository already
decided about it. There is no service, no database, and nothing to keep in sync:
the repository is the record, and a clone carries the whole thing.

What it does automatically: delivery. A record reaches the agent before it
edits a path.

What it assists with: capture. Recording a decision runs through
prepare → verify → stage, where the record is checked against the diff it claims
to describe.

What it does not do: deterministic autocapture. No host is certified to turn
every eligible commit into a terminal assessment on its own, and nothing in this
release claims otherwise. commitlore auto status now says so in its first line
instead of contradicting itself three lines later.

What a trust grade means, exactly

[directive] is a claim whose strength depends on how the repository is
configured, and the two modes are not the same promise:

mode [directive] means someone who can commit
author string (default) the author matched this repository's allowlist can produce one
signed git verified a signature on this repository's fingerprint allowlist cannot, without the key

[claim] means unverified provenance. [blocked] means the content matched an
injection pattern and was withheld. SECURITY.md states which failures are
vulnerabilities against which mode.

The measured result

On a preregistered comparison (bench/PREREGISTRATION-M5.md, frozen before the
run), an agent handed the repository's active records re-proposed a ruled-out
approach in 2.8% of runs (16/580) against 18.8% (109/579) without them.
bench/VERDICT-M5.md is the authority for those numbers and CI fails if any
README drifts from it.

Guard — the ruled-out matcher — is experimental and advisory: precision 44.8%,
recall 22.0%. An empty guard result is not a guarantee.


The protocol is Stable

spec/SPEC.md is 2.0 Stable. Within 2.x the grammar, the meaning of every
key, Record-Id's format and the X-<Name> extension slot do not change
incompatibly. Nothing in §3 is removed in 2.x; a key that must go is deprecated
first and removed no earlier than 3.0.

A record already committed stays readable regardless. The repository is the
record, and a reader that cannot read old commits has broken the one promise this
product makes.


Known limits, named

  • A launcher whose interpreter does not exist reports as a timeout on
    Windows.
    cmd.exe exits silently and nothing speaks the protocol, so a slow
    registration and a missing interpreter arrive as one report. Distinguishing
    them would mean reimplementing the host's command resolution, where being
    subtly wrong produces a confident false diagnosis.
  • A release reaches the CLI wrapper but not the Claude plugin cache. After
    installing, update the plugin and start a new session, then read
    runtime.build_id back to confirm which generation is answering.

v0.8.2

Choose a tag to compare

@github-actions github-actions released this 13 Aug 19:24
Immutable release. Only release title and notes can be modified.
59c6730

Five boundaries where CommitLore could tell you something had worked when it had not.

If you are upgrading

Nothing here changes the record format or how records are read, so no migration is needed. Two behaviours become stricter, and both fail loudly rather than silently:

  • An abbreviated or truncated git object id is no longer accepted where a canonical one is expected. If something in your setup persisted a short id, it is now rejected on read instead of being treated as though it named an object.
  • The installer exits non-zero when a host's MCP registration cannot actually serve MCP. An install that previously reported success while the integration was dead now reports the failure — and says which host and why.

What was wrong, and what it does now

A truncated object id validated as a canonical one. One pattern, [0-9a-fA-F]{4,64}, was doing two different jobs: matching a full object id and matching a revision a person typed. Under it, a 5-, 39-, 41- or 63-character string passed as canonical and was persisted and compared as though it named an object. Full ids are now exactly 40 or exactly 64 hex — the only two widths git has — and a branch, tag, HEAD~3 or abbreviation is resolved by git to one full id before it enters internal state.

A malformed request looked like an empty answer. The MCP boundary validated draft as a string and then cast each decoded record without checking its shape. A null, a number, or a record whose trailers was a string reached the verifier, which accepted nothing and replied validation_result: "empty" — byte-identical to what a session gets when it genuinely had no decision to record. That is the one distinction the capture contract rests on. Malformed input is now a caller error naming the offending record and field.

A hanging remote could stop your push. The pre-push notes sync ran git fetch and git push with no timeout. The surrounding code was fail-open, but a remote that accepts a connection and then goes quiet never reached the catch — the branch push simply stopped, with nothing explaining why. Those children are now bounded and non-interactive; a notes failure prints one line and the branch push continues.

The installer reported success for a host it never verified. A registration was considered fine because the config mentioned commitlore, or because the command string existed. A directory, a non-executable file, or the wrong arguments were skipped as "already configured" and the installer exited 0. Both installers now delegate to one shared command that treats ownership as the exact wrapper plus mcp, verifies a custom registration by speaking MCP to it, writes config atomically, and returns non-zero for any requested host that fails. When that inspection itself fails, the installer now says so instead of returning a bare status code.

The action that carries records across a squash could not write. It had no git identity, so on a default runner it detected the squash correctly and then died on Author identity unknown. With that fixed it immediately found the opposite defect: squashing a single commit keeps that commit's trailer block intact, so there was nothing to preserve and attaching anyway left two copies of one record. It now skips a record the merge commit already carries.

Also in this release

Capture no longer stages a reference the commit-msg hook would refuse. context builds the index it previously only read, bounded rather than walking all of history on every edit. Commits carrying only Signed-off-by: or Co-authored-by: are no longer refused as malformed records, so DCO and CommitLore can coexist in one repository.

The Node floor is now >=22.23.2, the current Node 22 LTS. node:sqlite is unflagged from 22.13, but its bundled SQLite only supplies the FTS5 virtual table from 22.16.0, so the previous >=22.12.0 floor silently used the slower path on 22.13–22.15.

What this release does not claim

Automatic capture on every coding agent, cryptographically authenticated directives by default, and organization-wide production readiness all remain out of scope. [directive] in the default mode still means the commit's author string matched a configured one — which anyone able to write a commit can set. Signer-to-authority binding is tracked in #597 and is not in this release.