From 6947cf9612ad252479191bbf6d7720411fd1c3ba Mon Sep 17 00:00:00 2001 From: MongLong0214 Date: Sat, 1 Aug 2026 13:05:18 +0900 Subject: [PATCH] Make the verified capture path the one the skill teaches The commits skill opened on hand-written trailers and reached the capture pipeline only as an aside, so an agent following it end to end produced a record that nothing had checked. Capture is the surface that binds a record to a nonce, hashes the transcript and staged diff it was drafted from, and refuses a quote that does not appear in those bytes; harvest checks the quotes but binds nothing to a HEAD, a diff or an expiry; a trailer block typed from memory is checked for shape alone. The skill defaulted to the weakest of the three. Capture is the body of the file now, in the five steps an agent actually executes -- prepare, draft, verify, stage, commit -- with the MCP tool names and argument shapes alongside the CLI equivalents, and with the part no prompt contract carries: the five gates prepare-commit-msg applies at commit time, and the five-minute window that opens at stage. Every command and tool name in it was run against a scratch repository before it was written down. Hand-writing moved to a fallback that states when it applies -- a commit that already exists, or a repository without the hooks -- and points at backfill for past commits, which reconstructs through the same verified loop instead of from memory. The harvest walkthrough went with it, and the drafted-JSON example with that: prepare's prompt prints both at the moment they are needed, so a second copy here was paid for by every session that loads this file, in order to teach the older path first. Record-Id: r-captureskill Limit: a skill loads into every agent's context, so a paragraph duplicating what the runtime prompt already prints is paid for on every session Ruled-out: Documenting both paths as equals | an agent takes the first workable path it reads, and the manual one produces a record no checker has seen Ruled-out: Dropping the vocabulary table along with the harvest walkthrough | it is what the fallback writes from and what a reader parses existing records with, and neither of those has a prompt contract to lean on Certainty: firm Blast: local Undo: easy Verified: prepare/verify/stage round-tripped over the real stdio MCP server and through commitlore capture --draft in a scratch repo, both reaching a commit whose trailer block the hook appended; a fabricated quote was refused as evidence-not-found at exit 0; manifest, doctor and agent-configs suites pass at 99; the file is 151 lines against 162 Unverified: whether an agent reading only this file installs the hooks it names -- the file points at commitlore-setup rather than restating init --- skills/commitlore-commits/SKILL.md | 221 ++++++++++++++--------------- 1 file changed, 105 insertions(+), 116 deletions(-) diff --git a/skills/commitlore-commits/SKILL.md b/skills/commitlore-commits/SKILL.md index c9cb54f7..4364af36 100644 --- a/skills/commitlore-commits/SKILL.md +++ b/skills/commitlore-commits/SKILL.md @@ -1,30 +1,96 @@ --- name: commitlore-commits -description: Use when about to make a git commit and there is decision context worth recording — a constraint that shaped the change, an alternative that was tried and dropped, a warning for whoever touches this next. Builds a CommitLore trailer block for the commit message, or drives the harvest pipeline to draft and machine-verify one from the session transcript and diff. Trigger phrases include "commit this with commitlore", "write a commitlore record for this change", "what should I record about why I ruled out X", "harvest a commit message", "commitlore 기록 남겨서 커밋해줘", "이 변경 결정 맥락 커밋에 남겨줘". +description: Use when about to make a git commit and there is decision context worth recording — a constraint that shaped the change, an alternative that was tried and dropped, a warning for whoever touches this next. Drives the CommitLore capture pipeline, which drafts a record from the session transcript and the staged diff, machine-checks every quote against them, and binds what survives to the commit it was prepared for. Trigger phrases include "commit this with commitlore", "write a commitlore record for this change", "capture the decision context for this commit", "what should I record about why I ruled out X", "commitlore 기록 남겨서 커밋해줘", "이 변경 결정 맥락 커밋에 남겨줘". --- # CommitLore commits A CommitLore record is the trailer block at the end of a commit message — -ordinary git trailers, parsed by `git interpret-trailers`. It captures what -the diff itself cannot show: the conditions that shaped the decision, the -alternatives that were dropped and why, and warnings for the next person (or -agent) who touches this code. +ordinary git trailers, parsed by `git interpret-trailers`. It captures what the +diff itself cannot show: the conditions that shaped the decision, the +alternatives that were dropped and why, and warnings for the next agent or +person who touches this code. + +Record through **capture**. It binds the record to a nonce, hashes the +transcript and staged diff it was drafted from, and refuses any quote absent +from those bytes — so a record citing something nobody said never reaches +history. Hand-writing trailers skips all of that; it is the fallback at the end +of this file, not the default. ## When to record, and when not to Trivial commits — typo fixes, formatting, a rename with no behavior change — -get no trailers. A record costs a future reader attention; spending that on -noise is worse than recording nothing. Only write one when there's a real -constraint, a real alternative that was seriously considered and rejected, or -a real warning to leave behind. +get no trailers. A record costs a future reader attention, and spending that on +noise is worse than recording nothing. Record only a real constraint, a real +alternative that was seriously considered and rejected, or a real warning worth +leaving. Answering `{"records": []}` is correct, and common. + +## Capture + +Needs the `prepare-commit-msg` hook that `commitlore init` installs (see +`commitlore-setup`); without it nothing staged reaches a commit message. Stage +the change first — capture hashes `git diff --cached`. + +**1. Prepare.** Write the relevant part of the session to a transcript, in the +words actually exchanged rather than a summary: it is the source every quote is +checked against, so paraphrasing is how a record ends up citing a sentence that +was never said. + +- MCP: `commitlore_prepare_capture { transcript }` → `{ nonce, prompt, + guard_advisory, policy_error, ... }` +- CLI: `commitlore capture --transcript session.txt` prints the same prompt. + +**2. Draft.** That `prompt` is a self-contained contract — the full vocabulary, +the rule *cite or omit*, and the JSON to answer in: a `records` array, each with +`trailers` (`key`, `value`) and `evidence` (`key`, `source` of `transcript` or +`diff`, `quote`, `locator`). A quote is copied character for character; a +locator is `L-L` for transcript lines or the `@@ ... @@` hunk header +for the diff. Follow the printed contract — it is the authority, and it carries +rules this file does not repeat. + +**3. Verify.** `commitlore_verify_capture { nonce, draft, transcript, diff }`, +where `draft` is that JSON as a string and `diff` is the same `git diff +--cached` bytes prepare hashed. Returns `validation_result` (`pass` | `partial` +| `empty`), `accepted`, and `rejected` with a reason each: `evidence-not-found` +(the quote is not in the source), `ruled-out-no-rejection` (the quoted passage +proposes the alternative rather than turning it down), `canonical-duplicate`. A +refused record is discarded and logged, never silently corrected. + +**4. Stage.** `commitlore_stage_capture { nonce }` → `{ "staged": true, +"nonce": "..." }`, or `{ "staged": false, "reason": "..." }` when verification +came back empty. Staging is what stamps `expires_at`. + +**5. Commit.** `git commit` as usual, message body only — the hook appends the +trailer block itself, so do not write trailers by hand or paste the draft in. It +applies the record only while all five hold: HEAD unchanged, staged diff +unchanged, under five minutes since staging, record unconsumed, capture policy +unchanged. Break one and the commit proceeds carrying no record. + +The CLI runs steps 1, 3 and 4 in a single command: -## The vocabulary +``` +commitlore capture --transcript session.txt --draft draft.json +``` +``` +staged: f13afcf766455ae46f6b1b4e96914f26 +``` -Sixteen keys, all optional, no others accepted. Anything outside this list -(other than an `X-:` extension) is rejected by `commitlore validate`. +A refusal prints its reason, stages nothing, and still exits 0 — capture is +never allowed to block the commit it sits next to: -### Decision context +``` +no record staged +commitlore: discarded record 0 (evidence-not-found): Limit: the transcript does not contain "the endpoint fails every third request" +``` + +`commitlore pending ls` lists transactions that have not reached a commit yet; +`commitlore capture gc` removes expired ones. + +## The vocabulary + +Sixteen keys, all optional, no others accepted — anything else (bar an +`X-:` extension) is rejected by `commitlore validate`. The capture prompt +reprints this, so the table is mostly for reading records and for the fallback. | Key | Value grammar | Repeatable | Meaning | |---|---|---|---| @@ -36,11 +102,6 @@ Sixteen keys, all optional, no others accepted. Anything outside this list | `Certainty:` | `firm` \| `tentative` \| `guess` | no | How sure the author is | | `Verified:` | free text | yes | What was checked, and how | | `Unverified:` | free text | yes | A known gap in verification | - -### Identity, lifecycle, provenance - -| Key | Value grammar | Repeatable | Meaning | -|---|---|---|---| | `Record-Id:` | `r-[a-z0-9]{6,}` | no | Stable identity for this record | | `Follows:` | `Record-Id` | yes | The prior record in a decision chain | | `Supersedes:` | `Record-Id` | yes | Retires an earlier record | @@ -50,18 +111,26 @@ Sixteen keys, all optional, no others accepted. Anything outside this list | `CommitLore-Version:` | semver | no | Protocol version this record targets | | `X-:` | free text | yes | Organization extension, never interpreted by the core | -Enum values must match exactly — `Blast: wide`, `Undo: clean`, and -`Certainty: high` are all violations, not accepted synonyms. `Record-Id` is a -random-looking identifier, not a hash of the commit, so it survives a rebase -or squash; `Follows:`/`Supersedes:` reference a `Record-Id`, never a sha. +Enums must match exactly — `Blast: wide`, `Undo: clean` and `Certainty: high` +are violations, not synonyms. `Record-Id` is random rather than a hash of the +commit, so it survives a rebase or squash, and `Follows:`/`Supersedes:` +reference one, never a sha. `Verified:` is the one key capture never drafts — +reading a transcript cannot prove a check ran. + +The block must be the message's **last** paragraph, every line a `Key: value` +line or an indented continuation. Mix in one line of ordinary prose and the +whole paragraph parses as prose — zero trailers, not a partial record. -The trailer block must be the message's **last** paragraph, and every line in -it must be a `Key: value` line or an indented continuation of one. Mix in a -line of ordinary prose and the whole paragraph is read as prose — zero -trailers parsed, not a partial record. Keep the trailer block separated from -the body by a blank line and don't add commentary inside it. +## Fallback: writing the block by hand -Check a message before committing it: +Capture binds a record to a HEAD and a staged diff, so it cannot record a +decision for a commit that already exists, and it does nothing where the hooks +were never installed. Those are the cases for writing trailers yourself. For +past commits that never carried a record, prefer `commitlore backfill +--prompt-only` / `--draft`: it reconstructs through the same verified loop and +marks every result `Provenance: reconstructed`. + +Check a hand-written message before committing it: ``` printf 'Widen the retry window\n\nBlast: wide\n' | commitlore validate @@ -70,93 +139,13 @@ printf 'Widen the retry window\n\nBlast: wide\n' | commitlore validate 3: enum Blast — got "wide", want "local|module|system" commitlore: 1 violation (SPEC §6) — the message was not modified ``` -(exit 1). A message with valid trailers exits 0 and prints nothing. If -`commitlore-setup`'s commit-msg hook is installed, this check runs -automatically on every `git commit`; running `validate` by hand first is just -a faster feedback loop while drafting the message. - -## The harvest pipeline -Writing trailers by hand from memory is optional — the alternative is to hand -the drafting to the current agent session and let a separate, deterministic -checker throw out anything it made up. - -**1. Get the prompt contract.** Point `commitlore harvest` at a transcript of -the session and the diff being committed: - -``` -commitlore harvest --transcript session.txt --diff staged.diff --prompt-only -``` - -With no `--diff`, it uses the staged diff. This prints a self-contained -prompt: the same vocabulary tables above, the rule "cite or omit — a missing -record is better than a false one", the exact JSON shape to answer in, and -the transcript and diff themselves, line-numbered. Nothing here calls a -model — this command only builds the prompt text. The session reading it (the -one already in context, with no separate API key or cost) answers by -producing a JSON draft: one object per record, each `trailers` entry paired -with `evidence` entries that quote the transcript or diff verbatim and give a -locator (`L-L` for a transcript line range, or the diff's `@@` -hunk header). Save that answer to a file, e.g. `draft.json`. - -**2. Verify it.** Nothing above is trusted yet — a session can misquote or -invent a rejection that never happened. `commitlore harvest-verify` checks -the draft against the same transcript and diff, deterministically, with no -model in the loop: - -``` -commitlore harvest-verify --draft draft.json --transcript session.txt --diff staged.diff -``` - -A record that clears every check prints in `{"records": [...]}` form, ready -to fold into the commit message. Verified example — transcript said *"...A -queue-based retry would fix it too, but the free-tier infra has no queue -worker, so that option is ruled out for now..."*: - -``` -{ - "records": [ - { - "trailers": [ - { "key": "Limit", "value": "the endpoint is flaky and fast retries trip the upstream rate limiter" }, - { "key": "Ruled-out", "value": "queue-based retry | the free-tier infra has no queue worker" } - ], - "evidence": [ - { "key": "Limit", "source": "transcript", "quote": "tripping the upstream rate limiter", "locator": "L1-L2" }, - { "key": "Ruled-out", "source": "transcript", "quote": "the free-tier infra has no queue worker", "locator": "L4-L4" } - ] - } - ] -} -``` - -A record that quotes something nobody actually said is discarded and logged, -never silently dropped or "corrected": - -``` -{ - "records": [] -} -``` -``` -commitlore: discarded record (evidence-not-found): Limit: the transcript does not contain "the endpoint fails every third request" -``` +(exit 1; a valid message exits 0 and prints nothing). The commit-msg hook runs +this on every commit once `commitlore-setup` has installed it. -`Ruled-out:` gets an extra check beyond the quote existing: the text around -the quote has to show the alternative actually being turned down (a phrase -like "ruled out", "instead", "not viable"), not just mentioned. Quoting the -sentence where an alternative was first *proposed* is rejected as -`ruled-out-no-rejection`, with the same "discard, don't fabricate" policy. - -**3. Repair, bounded.** `--repair-prompt` on a run with rejections prints -feedback text naming exactly what failed and why, meant to be handed back to -the session for one more attempt. This is capped at two rounds — after that, -the commit proceeds with whatever passed, or with nothing. Both -`harvest --prompt-only` (nothing to harvest) and `harvest-verify` -(everything rejected) always exit 0: an optional enrichment step is not -allowed to block the commit it sits next to. - -**4. Commit.** Fold the trailers from the surviving `records` into the commit -message's trailer block (last paragraph, one `Key: value` line per trailer) -and commit as usual. If the setup skill's hook is installed, `commitlore -validate` runs on the final message automatically. +`commitlore harvest --transcript session.txt --prompt-only` and `commitlore +harvest-verify --draft draft.json --transcript session.txt --diff staged.diff` +give the same contract and evidence checking without the transaction: quotes are +verified, but nothing binds the result to a HEAD, a diff or an expiry, and the +survivors must be folded into the message by hand (`--repair-prompt` emits +feedback for one more attempt). Reach for these only when capture cannot bind.