claim-ledger — every PR claim, tagged with the evidence you actually hold
The spine, one question:
For each behavior this PR claims, what is the strongest evidence I hold that it is true — and would I publish that evidence class next to the claim?
When you wrote code by hand, verification happened in your head while typing. When an agent writes it, that step has no home: the agent produces, you read, and the first real execution happens in a peer's head during review. That is why they find bugs you didn't. This skill gives verification a home before the PR opens, and — the part that matters more — tells you afterward which kind of miss each review comment was.
self-audit asks "is this diff clean?" test-fidelity asks "do these tests
constrain the code?" This skill asks "what did I actually verify, and what
did I merely believe?" It does not replace either; it wraps them.
Evidence classes
| Tag | Means | Requires |
|---|---|---|
RAN |
Executed against real code, observed the outcome | Command, exit code, output captured under commands/ |
READ |
Traced without executing | file:line citations, or a fresh-context adversarial verdict on this claim |
ASSUMED |
Not verified | Reason from the closed set below + one line naming the blocker |
UNLISTED |
(reconcile only) A reviewer found a claim you never enumerated | — |
ASSUMED reasons (closed set):
harness— could not execute: needs creds / a cluster / 40 minutes / is flaky. Name the exact blocker.context— do not know what a consumer or sibling package assumes. Name the consumer.budget— chose not to. Say so plainly.
Iron rules
- Correlated evidence does not upgrade. A test written in the same
session as the code it tests shares that session's misunderstanding. It is
READuntil it passestest-fidelity's spine question. Green is notRAN. - Every claim carries exactly one tag. Rows start as
ASSUMEDand are upgraded by evidence. An untagged claim is a lie of omission; the ledger is complete or it is not a ledger. - Reasons are a closed set. Free-text reasons do not aggregate, and
aggregation across PRs is the entire point of
reconcile. - The residue ships. The
ASSUMEDblock goes into the PR description verbatim. Discomfort about publishing a row is the signal to go discharge it — never to delete it. buildwithoutreconcileis a checklist.reconcileis what turns it into measurement. Skipping it is how you end up guessing "harness vs. skill" for another quarter.- Durable directory, never session tmp.
~/claim-ledgers/<org>-<repo>/<branch>/withledger.md,residue.md,commands/. Session scratch gets garbage-collected mid-run; that failure is already on the books incounterfactual-audit.
Where claims come from — three sources, all mandatory
1. Stated. PR title, description draft, every commit message. Each verb phrase is a claim ("retries on 5xx", "no behavior change for existing callers"). Negative claims count double — "backward compatible" is the claim most often assumed and least often run.
2. Callers-unaffected. Every changed exported symbol implies "existing
callers still behave." Enumerate callers with mache (find_callers,
get_impact --max-depth 2); fall back to rg -w <Symbol> with the
unqualified-call discipline from counterfactual-audit pass 2. Cluster by
consuming package: one claim per consuming package, not per call site.
This is where monorepo breadth becomes finite — a claim list, not a feeling.
3. Invariants-preserved. For every touched package read the nearest
AGENTS.md, CLAUDE.md, CONTRIBUTING.md, or invariants doc. Each stated
invariant the diff could plausibly violate is a claim. If a touched package
has no such file, record that as a row — it is an UNLISTED finding
waiting to happen, and reconcile will tell you to write it.
Size calibration. A reviewable PR yields 5–20 claims. More than 20 → the
PR is too big; stop and run work-scope. Fewer than 5 on a 300+ line diff →
you are under-enumerating; go back to source 2.
build — before the PR opens
Step 0 — Resolve and set up
BASE="${1:-main}"
SHA=$(git rev-parse HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
ORG_REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner | tr '/' '-')
LEDGER_DIR="$HOME/claim-ledgers/$ORG_REPO/$BRANCH"
mkdir -p "$LEDGER_DIR/commands"
git diff --stat "$BASE...HEAD"
git log --format='%h %s%n%b' "$BASE..HEAD"
Step 1 — Enumerate
Write ledger.md with every row from all three sources, all tagged
ASSUMED, before discharging any of them. Enumerate first, discharge
second; interleaving means you fix the first row and never finish the list.
| # | Claim | Source | Tag | Evidence / reason |
|---|---|---|---|---|
| 1 | Retries on 5xx up to 3× with backoff | stated | ASSUMED | — |
| 2 | pkg/ingest callers of `Fetch` unaffected (14 sites) | callers | ASSUMED | — |
| 3 | `AGENTS.md`: writers never hold the lock across a network call | invariant | ASSUMED | — |
| 4 | pkg/export has no invariants doc | invariant | ASSUMED | context: none exists |
Step 2 — Discharge, highest blast radius first
Sort by caller count descending. For each row:
Try RAN. Is there a Taskfile target or test that exercises this
claim? Run it. Capture stdout/stderr and exit code to
commands/<row>-<slug>.txt. If the test was written this session, it stays
READ until test-fidelity says it constrains the code (rule 1).
If the harness blocks you — needs a cluster, needs creds, takes too long,
flakes — write the blocker verbatim as the harness reason and move on.
Do not fight the harness during build. An hour lost to a flaky integration
suite is an hour not spent on the other 14 rows. The gap log (step 4) is where
that hour gets repaid, as its own PR.
Else READ. Trace by hand and cite file:line for the path that makes
the claim true. Or dispatch a fresh-context subagent — never a fork, which
inherits the writer's assumptions — scoped to this one claim:
Here is a diff and one claim about it:
<claim>. Find the input, caller, or sequence under which the claim is false. Cite file:line. If you find nothing, say "holds" — do not manufacture concerns.
The reader is uncorrelated with the writer. That is its only value; a "holds" from the same context that wrote the code is worth nothing.
Else ASSUMED stays, with its reason.
Step 3 — Emit residue.md
This block goes into the PR description verbatim. Flat tone: it is a map for the reviewer, not a confession.
## Verification
RAN (n): #1 retry/backoff (`task test:pkg/client`), #2 …
READ (n): #3 lock invariant (traced `writer.go:88-114`; adversarial pass: holds)
### Not verified — reviewer attention here
- [harness] #5 export job end-to-end — blocked by: integration suite needs a live GCS bucket
- [context] #7 pkg/billing callers of `Amount` — unknown: whether billing rounds before or after
- [budget] #9 log line format — chose not to; cosmetic
Step 4 — Log harness gaps durably
Every harness-reasoned row is appended to
~/.claude/skills/claim-ledger/harness-gaps.jsonl — outside any git repo:
{"ts":"2026-09-02","repo":"org/repo","blocker":"integration suite needs a live GCS bucket","claim":"export job end-to-end"}
Three hits on the same blocker is a bead. That backlog is the platform
work; the fix is almost always taskfile-ci-parity on the top entry.
Step 5 — Stop
build produces no fixes. Findings during discharge become new commits
after the ledger is complete (self-audit's no-amend rule applies), and
the ledger row is re-tagged when they land.
reconcile — after review, mandatory
Step 1 — Fetch correctness comments
N=<pr>; OWNER=<org>; REPO=<repo>
gh api "repos/$OWNER/$REPO/pulls/$N/comments" \
--jq '.[] | {path, line, user: .user.login, body}'
gh api "repos/$OWNER/$REPO/pulls/$N/reviews" \
--jq '.[] | select(.body != "") | {user: .user.login, state, body}'
Keep comments about behavior — wrong output, missed case, race, broken caller, violated invariant. Drop nits, style, naming, and bot output (list bot logins in the extensions file). When a comment is ambiguous, ask the user one question, not a battery.
Step 2 — Map each comment to a ledger row, or UNLISTED
Step 3 — Classify and act
| Landed on | What it means | Action |
|---|---|---|
RAN |
Your verification lied | Dispatch test-fidelity on that test. It is theatre until fixed. |
READ |
Reading missed it | Record diff size and whether a fresh-context pass ran on that row. This is the attention signal. |
ASSUMED |
The system worked — the reviewer looked where you pointed | No fault. Count it. If harness-reasoned, the gap-log entry gets a hit. |
UNLISTED |
You did not know it was a claim | Write the invariant into the nearest AGENTS.md/CLAUDE.md for that package — in this PR or a follow-up. This is the context loop closing. |
Step 4 — Tally
Append one line per comment to ~/.claude/skills/claim-ledger/tally.jsonl:
{"ts":"2026-09-05","repo":"org/repo","pr":412,"landed":"UNLISTED","reason":null,"pkg":"pkg/billing"}
Then print the distribution over the last 10 PRs.
Reading the tally
| Dominant class | Verdict | What to change |
|---|---|---|
ASSUMED[harness] |
Harness problem | The gap log is your backlog. taskfile-ci-parity on the top blocker, as its own PR. |
UNLISTED |
Context problem | Every reconcile writes one invariant down. Six months of this is the monorepo's missing AGENTS.md tree. |
READ |
Attention problem | Smaller diffs (work-scope), and the fresh-context reader on every row, not just the ones that felt risky. |
RAN |
Tests are lying | test-fidelity on every test-bearing PR before it opens. |
"Harness" is a valid verdict. So is "skill." So is "both, in this ratio." The point is to stop guessing.
Calibration
- Zealotry — a claim per hunk, a 60-row ledger nobody reads. Claims are behaviors, not lines. Cluster callers by package. Aim for 5–20.
- Honesty theatre —
ASSUMED[budget]on everything. The ledger is true and useless. More than 50%ASSUMED→ stop; discharge rows or shrink the PR. Do not open it. - Fighting the harness mid-build — see step 2. Log the gap, move on.
- Residue as apology — hedging language in the residue invites the reviewer to skim it. Flat rows, one line each.
- Reconciling only the bad PRs — a PR with zero correctness comments is a data point too. Record it, or the tally is survivorship-biased.
Common mistakes (hypotheses — earn or delete after five reconciles)
| Mistake | Expected consequence |
|---|---|
| Ledger in session tmp | Lost mid-run; reconcile has nothing to map against |
Treating a same-session test as RAN |
The row that the reviewer's bug lands on, every time |
| Skipping source 2 because "it's internal" | Monorepo consumers are exactly where UNLISTED comes from |
Free-text ASSUMED reasons |
Tally cannot aggregate; verdict stays a vibe |
| Deleting an uncomfortable residue row | You just shipped the bug with less reviewer attention on it |
Project-specific extensions (optional)
If ~/.claude/skills/claim-ledger/extensions.md exists, read it at the start
of build step 2 and reconcile step 1. It layers, per repo:
- The two-minute harness subset — the fastest Taskfile target that
exercises each package, so
RANis reachable more often. - Where invariant docs live if not at the conventional paths.
- Bot logins to drop from comment fetches.
OWNER/REPOdefaults forgh.
Absent the file, the procedure is generic and degrades gracefully.
Why this exists separately
| Skill | Question | Surface |
|---|---|---|
self-audit |
Is this diff clean? | The diff |
test-fidelity |
Do the tests constrain the code? | The tests |
review-prep |
What is the blast radius? | The graph |
claim-ledger |
What did I verify vs. believe — and which kind of miss was that? | The claims, across PRs |
review-prep feeds source 2. test-fidelity gates rule 1. self-audit step
7 is one way to discharge a READ. taskfile-ci-parity is what the gap log
turns into. work-scope is where a 30-row ledger sends you.
Cross-references
- Evidence tagging lineage:
counterfactual-audit(OBSERVED / INFERRED / PROPOSED) — same discipline, pointed at a diff instead of an architecture. - Callers:
mache-usage,review-prepsteps 6–8. - Judgment on
RAN:test-fidelity. - Fresh-context reader:
self-auditstep 7 prompt, scoped per claim. - Where the gap log goes:
taskfile-ci-parity.