Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "commitlore",
"displayName": "CommitLore",
"version": "1.2.4",
"version": "1.2.5",
"description": "Recorded decisions from git history, delivered to the agent before it edits. Constraints, alternatives already ruled out, and warnings left by whoever was here last.",
"author": {
"name": "MongLong0214",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "commitlore",
"version": "1.2.4",
"version": "1.2.5",
"description": "Decision memory from Git history, with verified capture for coding sessions.",
"author": {
"name": "MongLong0214",
Expand Down
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,65 @@ Release notes for 1.0.0, 1.0.1 and 1.0.2 are on the
[GitHub releases page](https://github.com/MongLong0214/commitlore/releases); they
were not written here.

## 1.2.5

Two flags on `capture` did nothing and said nothing; a third refusal knew the
fix and did not say it.

**`capture --out` exited 0 and never wrote the file (#878).** The flag is
documented as "write the pending nonce to a file", and prompt-only mode — get the
nonce, hand the prompt to a model, come back with `--draft` — is the step it
exists for. That was the one run where the pipeline reported `nonce: null` while
prepare had already persisted a real transaction under a real nonce, so the write
was guarded out. Measured by the reporter as 3 runs, 3 exit 0, 3 missing files,
with and without `--diff`. The nonce is now reported, so `--out` writes it and
`--json` carries it.

**`capture --diff` was accepted, ignored by prepare, and honoured by verify
(#877).** Passing the *same* `--diff` file to both steps failed whenever that
file was not byte-identical to the staged diff:
`discarded record 0 (source-mismatch): diff hash does not match the prepared
transaction`. That message is about the draft's sources, and the fault was
entirely the flag's — the reporter spent several attempts re-checking quotes and
locators that were never wrong.

The refusal itself was right and stays. A capture transaction binds to the staged
diff, which prepare, verify and stage each recompute independently, because every
binding is computed server-side and never from the caller. `--diff` cannot select
a different diff; it can only assert what is staged. So it is now refused where
that is decidable — up front, exit 2, naming the flag and naming the way out
(`git reset --soft`, which is what the reporter had to find on their own) — rather
than several steps later against the record. A run refused this way also leaves no
pending transaction behind; the old path wrote two per attempt.

`--diff` byte-identical to the staged diff keeps working, and the help text now
says what the flag does rather than implying an override it never had.

**`unknown-key` now names the `X-` form the author could have written (#881).**
Claude Code instructs every session to end its commit message with
`Claude-Session:`, so the key arrives by default, the hook refuses it, the commit
is lost, and an agent that cannot see the repository's earlier commits writes it
again. Reported twice in one day in two repositories by the same author, the
second time after already knowing about the first, because `X-` is not the kind of
thing that stays in mind between repositories.

```
31: unknown-key Claude-Session — got "Claude-Session",
want "a key from SPEC §3, or X-Claude-Session if this is your own metadata"
```

Nothing is accepted that was not accepted before, and nothing is rewritten: SPEC
§6 says the message is not modified, and silently renaming someone's trailer would
be worse than refusing it. This only says the name. Two keys are deliberately not
offered the prefix — one whose value reads as a sentence, which keeps the #647
answer, because that author's problem is that their prose became metadata; and one
that differs from a SPEC §3 key only by case, because `X-limit` would be a valid
record carrying the wrong key.

`spec/fixtures/invalid/03-unknown-key.expected.json` moves with it. The violation
it pins is unchanged and `Constraint:` is still refused; only the advisory `want`
text differs.

## 1.2.4

`doctor` blamed the hook that worked, and prescribed reinstalling it.
Expand Down
12 changes: 6 additions & 6 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@
</p>

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

<details>
<summary>先にインストーラーを読みたいですか?</summary>

```bash
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh
sh install.sh v1.2.4
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh
sh install.sh v1.2.5

# あるいはスクリプトを使わずに。スクリプトが作るチェックアウトは自分でも作れます。
git clone --depth 1 --branch v1.2.4 https://github.com/MongLong0214/commitlore
git clone --depth 1 --branch v1.2.5 https://github.com/MongLong0214/commitlore
node commitlore/dist/commitlore.mjs --version
```

Expand Down Expand Up @@ -107,13 +107,13 @@ CommitLore はその判断をコードのそばに残します。
macOS と Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

Windows:

```powershell
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1))) v1.2.4
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1))) v1.2.5
```

Node.js 22.23.2+ と Git が必要です。スクリプトは何かを書き込む前に両方を確認します。
Expand Down
12 changes: 6 additions & 6 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@
</p>

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

<details>
<summary>먼저 설치기를 읽어 보고 싶나요?</summary>

```bash
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh
sh install.sh v1.2.4
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh
sh install.sh v1.2.5

# 또는 스크립트를 건너뜁니다. 스크립트가 만드는 체크아웃은 직접 만들 수 있습니다.
git clone --depth 1 --branch v1.2.4 https://github.com/MongLong0214/commitlore
git clone --depth 1 --branch v1.2.5 https://github.com/MongLong0214/commitlore
node commitlore/dist/commitlore.mjs --version
```

Expand Down Expand Up @@ -107,13 +107,13 @@ CommitLore는 그 판단을 코드 곁에 보관합니다.
macOS와 Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

Windows:

```powershell
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1))) v1.2.4
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1))) v1.2.5
```

Node.js 22.23.2+와 Git이 필요합니다. 스크립트는 무엇이든 쓰기 전에 둘을 확인합니다.
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@
</p>

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

<details>
<summary>Prefer to read the installer first?</summary>

```bash
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh
sh install.sh v1.2.4
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh
sh install.sh v1.2.5

# Or skip the script: the checkout it makes is one you can make yourself.
git clone --depth 1 --branch v1.2.4 https://github.com/MongLong0214/commitlore
git clone --depth 1 --branch v1.2.5 https://github.com/MongLong0214/commitlore
node commitlore/dist/commitlore.mjs --version
```

Expand Down Expand Up @@ -109,13 +109,13 @@ preserve, not for narrating every change.
macOS and Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

Windows:

```powershell
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1))) v1.2.4
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1))) v1.2.5
```

Requires Node.js 22.23.2+ and Git. The script checks both before it writes anything.
Expand Down
12 changes: 6 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@
</p>

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

<details>
<summary>想先阅读安装器吗?</summary>

```bash
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh
sh install.sh v1.2.4
curl -fsSLO https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh
sh install.sh v1.2.5

# 或者跳过脚本:它创建的检出,你自己也能创建。
git clone --depth 1 --branch v1.2.4 https://github.com/MongLong0214/commitlore
git clone --depth 1 --branch v1.2.5 https://github.com/MongLong0214/commitlore
node commitlore/dist/commitlore.mjs --version
```

Expand Down Expand Up @@ -105,13 +105,13 @@ CommitLore 把那份判断留在代码旁边。
macOS 和 Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
```

Windows:

```powershell
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1))) v1.2.4
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1))) v1.2.5
```

需要 Node.js 22.23.2+ 和 Git。脚本会在写入任何内容前检查两者。
Expand Down
4 changes: 2 additions & 2 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<#
Installs commitlore from source on Windows, for any agent that is not Claude Code.

irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1 | iex
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.ps1))) v1.2.4
irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1 | iex
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.ps1))) v1.2.5

Claude Code users do not need this script. The repository is itself a plugin
marketplace (ADR-0011), so two /plugin commands register the MCP server, the
Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/bin/sh
# Installs commitlore from source, for any agent that is not Claude Code.
#
# curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4
# curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5
#
# **Claude Code users do not need this script.** The repository is itself a
# plugin marketplace (ADR-0011), so two `/plugin` commands register the MCP
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "commitlore",
"version": "1.2.4",
"version": "1.2.5",
"description": "Git-native, lifecycle-aware decision memory for coding agents",
"license": "MIT",
"private": true,
Expand Down
6 changes: 3 additions & 3 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"source": "github"
},
"websiteUrl": "https://github.com/MongLong0214/commitlore#readme",
"version": "1.2.4",
"version": "1.2.5",
"_meta": {
"io.modelcontextprotocol.registry/publisher-provided": {
"registryFit": "Distribution is a tagged git checkout plus a Claude Code plugin marketplace (ADR-0011 registry-free git distribution, ADR-0026 no compiled executables and no uploaded release asset), so no official package type applies and this record relies on websiteUrl plus publisher metadata.",
Expand All @@ -18,8 +18,8 @@
"/plugin marketplace add MongLong0214/commitlore",
"/plugin install commitlore@commitlore"
],
"installer": "curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.4/install.sh | sh -s v1.2.4",
"release": "https://github.com/MongLong0214/commitlore/releases/tag/v1.2.4"
"installer": "curl -fsSL https://raw.githubusercontent.com/MongLong0214/commitlore/v1.2.5/install.sh | sh -s v1.2.5",
"release": "https://github.com/MongLong0214/commitlore/releases/tag/v1.2.5"
},
"runtime": {
"transport": "stdio",
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/invalid/03-unknown-key.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
{ "key": "Constraint", "value": "must ship by friday per the compliance deadline" }
],
"violations": [
{ "key": "Constraint", "value": "must ship by friday per the compliance deadline", "rule": "unknown-key", "got": "Constraint", "want": "a key from SPEC §3 or X-<Name>" }
{ "key": "Constraint", "value": "must ship by friday per the compliance deadline", "rule": "unknown-key", "got": "Constraint", "want": "a key from SPEC §3, or X-Constraint if this is your own metadata" }
]
}
54 changes: 44 additions & 10 deletions src/commands/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,37 @@ const runCapturePipeline = (opts: {
const { transcriptPath, diffPath, draftPath, cwd } = opts;

const transcript = readCallerFile(transcriptPath);
// Prepare hashes `git diff --cached` itself, so verification has to be given
// the same bytes. This used to default to the empty string, whose hash never
// matches -- every record was refused with `source-mismatch` and the command
// printed `no record staged`, so `capture --draft` could not succeed at all
// unless the caller happened to pass a --diff file byte-identical to the
// staged diff. A caller-supplied --diff that differs is still a real mismatch
// and is still refused.
const diff = diffPath ? readCallerFile(diffPath) : execGitOrThrow(['diff', '--cached'], { cwd });
// The transaction binds to the staged diff and only to that: prepare hashes
// `git diff --cached` itself, and stage recomputes it a third time before
// writing, because every binding is computed server-side and never from the
// caller (capture-stage.ts). So `--diff` can assert what is staged; it cannot
// override it.
//
// It used to default to the empty string, whose hash never matches -- every
// record was refused with `source-mismatch` and the command printed `no record
// staged`, so `capture --draft` could not succeed at all unless the caller
// happened to pass a --diff file byte-identical to the staged diff.
//
// A --diff that differs is still refused. What #877 is about is where that
// refusal surfaced: prepare wrote a pending file for a run that could not
// succeed, and verify then blamed the draft -- `source-mismatch: diff hash
// does not match the prepared transaction` -- for a fault belonging entirely
// to the flag. The reporter re-checked quotes and locators that were never
// wrong. It is refused here instead, before prepare, naming the flag.
const callerDiff = diffPath === undefined ? undefined : readCallerFile(diffPath);
const diff = execGitOrThrow(['diff', '--cached'], { cwd });
if (callerDiff !== undefined && callerDiff !== diff) {
throw markCaptureError(
new Error(
`--diff ${JSON.stringify(diffPath)} is not the staged diff. A capture transaction ` +
'binds to the staged diff -- prepare, verify and stage each recompute it, so --diff ' +
'can assert what is staged but cannot override it. Stage the change you are ' +
'recording; to record against a commit that already exists, soft-reset it first ' +
'(git reset --soft HEAD~1).',
),
'usage',
);
}

// 1. Prepare: compute bindings, generate prompt, persist prepared transaction
const prepareResult = prepareCaptureContext({
Expand All @@ -239,10 +262,17 @@ const runCapturePipeline = (opts: {
}

// 2. If no draft provided, print the prompt contract and exit (prompt-only mode)
//
// The nonce is reported, not dropped (#878). Prepare has already persisted the
// transaction under it, and prompt-only is the step `--out` exists for: get the
// nonce, hand the prompt to a model, come back with --draft. Returning null
// here made `if (options.out && result.nonce)` false on exactly that run, so
// --out wrote nothing and said nothing, and a scripted caller had no handle on
// which transaction it was completing until verify failed several steps later.
if (!draftPath) {
return {
outcome: 'empty',
nonce: null,
nonce: prepareResult.nonce,
staged: false,
prompt: prepareResult.prompt,
transcript_window: prepareResult.transcript_window,
Expand Down Expand Up @@ -396,7 +426,11 @@ export const register = (program: Command): void => {
'path to the session transcript file (the prompt carries its last 256 KiB; ' +
'COMMITLORE_TRANSCRIPT_BUDGET_BYTES changes that, and verification always reads all of it)',
)
.option('--diff <path>', 'path to the diff file (defaults to the staged diff)')
.option(
'--diff <path>',
'assert the staged diff equals this file; the transaction always binds to the staged diff, ' +
'so this cannot select a different one',
)
.option('--draft <path>', 'path to the draft JSON file (omit for prompt-only mode)')
.option('--out <path>', 'write the pending nonce to a file')
.option('--shadow', 'measure historical capture candidates without writing anything')
Expand Down
Loading
Loading