Skip to content

Session Evidence Ledger

Reference for the session evidence ledger (#1131, part of #1129) — the per-project, append-only record of what tooling sessions actually used: skills invoked, agents spawned, commands run. It is the observational substrate consumed by gap detection, demotion evidence, and orchestrator feedback. The ledger itself is deliberately dumb: no analysis logic lives in it.

Property Value
Location <project-root>/.claude/session-ledger.jsonl
Format JSON Lines — one event per line, append-only
Schema schemas/session-ledger.schema.json (validates a single line)
Git status Ignored — per-project machine state, like .claude/loop-notes.json
Owner (single source of truth) scripts/evidence/ledger.js append — the only writer; the collector hook and the transcript backfill both write through it

CI validates the committed sample fixture (schemas/fixtures/session-ledger.sample.jsonl) plus any live ledger present in the repo (check-schemas.js, block 8f).

Each line is one event:

Field Type Meaning
v integer Event format version (currently 1)
ts string ISO 8601 timestamp the event was observed
project string Basename of the project root the session ran in
machine string Fleet machine id (hostname-matched against machines.json, falling back to bare hostname)
harness string The agent harness that produced the event (claude-code today)
event_type enum invocation (an item was used) · improvised (the session hand-built something an item should cover) · no-trigger (a task ran with no skill/agent triggering). Collectors emit invocation; improvised/no-trigger are gap observations recorded via gap-analysis.js observe (#1134)
item_type enum skill · agent · command · task
item_name string Skill/command slug or agent type; a short task label for no-trigger/improvised
session_id string Harness session id, when known (may be empty)
source enum hook (live collector) · backfill (one-time transcript bootstrap) · observation (a deliberate gap observation, gap-analysis.js observe — #1134)
detail object? Optional collector extras (e.g. the raw tool name). Never required by consumers

The schema is harness-agnostic by contract: Claude Code is the first collector; a future harness adds its own collector writing the same format with its own harness value.

Matching convention: consumers joining ledger events against provisioned items should match by item_name, not item_type — the same registry name may surface as a skill or a command depending on harness wiring.

hooks/session-ledger-append.sh — a PostToolUse hook with matcher Skill|Task|Agent, wired in the canonical settings.json:

  • Skill calls append as item_type: skill (tool_input.skill)
  • Task / Agent calls append as item_type: agent (tool_input.subagent_type, defaulting to general-purpose)
  • All other tools are ignored

Fail-open everywhere. Missing node, missing script, unparseable stdin, unwritable disk — every path exits 0 and appends nothing. Evidence collection must never block or slow a session. Overhead is one short-lived node process per Skill/Task call; machine identity deliberately avoids the full fleet resolver (which may shell out to tailscale with a 3s timeout) in favor of a hostname match against machines.json.

Terminal window
node ~/.claude/scripts/evidence/backfill-transcripts.js # dry-run
node ~/.claude/scripts/evidence/backfill-transcripts.js --apply # write
node ~/.claude/scripts/evidence/backfill-transcripts.js --project X # scope to one project
node ~/.claude/scripts/evidence/backfill-transcripts.js --transcripts DIR # override ~/.claude/projects

Mines existing Claude Code transcripts (~/.claude/projects/*/) for historical Skill/Task/Agent invocations and appends them marked source: "backfill". This is a one-time bootstrap, not the ongoing mechanism — run it once when adopting the ledger so consumers start with history. Properties:

  • Dry-run by default — reports per-project counts; --apply writes.
  • Idempotent — an event whose (ts, session_id, item_type, item_name) already exists with source backfill is never appended twice; re-running is safe.
  • Non-fatal gaps — a transcript whose cwd no longer exists on this machine is counted and skipped.
Terminal window
node ~/.claude/scripts/evidence/ledger.js query [--cwd DIR] [--days N] [--json]
node ~/.claude/scripts/evidence/ledger.js unused [--cwd DIR] [--days N] [--universal] [--json]
  • query — “what did project X invoke in the last N days”: per-item count, item types, first/last seen, distinct sessions, sources. Sorted by count.
  • unused — “which provisioned items have zero ledger events”: joins the ledger against the project’s provision manifest (provisions/<project>.json, _default.json fallback — the same resolution cdprov applies). --universal widens the declared set to the always-present universal skills/commands/agents. This is the demotion-evidence question (#1132) and gap-detection input (#1134).

scripts/fleet/audit.js snapshot (#415) attaches a compact ledger block to each audited project:

{ "days": 30, "events": 57, "items": 12, "last_ts": "", "top": [{ "name": "Explore", "count": 19 }] }

null means the project has no ledger at all (distinct from a ledger with an empty window). Snapshots travel cross-machine over the existing #415 mechanism, so BOB_HOME-level decisions can weigh evidence from every machine a project runs on.

Two engines read the ledger (the ledger itself stays analysis-free):

  • Provisioning gapsscripts/orchestrator/gap-detect.js match --task "<text>": does an unprovisioned registry item cover the task at hand? Conservative frontmatter match, #412-aware dedupe (project manifest + this machine’s resolved _bob-home chain), emits the cdprov add <kind>/<name> --now fix with the automation-level action (L1 ask / L2 confirm / L3+ auto). Exit 0 = no gap, 3 = gap, 2 = usage.
  • Authoring gapsscripts/evidence/gap-analysis.js: observe appends improvised / no-trigger events (source observation); analyze applies the recurrence threshold (_workflow.gap_recurrence_count default 3 across ≥2 sessions) with cross-project routing (≥2 projects → registry candidate, else project-docs); recommend [--apply] files the qualifying batch as authoring-gap issues routed toward skill-creator; dismiss suppresses a declined recommendation until new evidence accrues (memory: .claude/.gap-recommendations.json). Hosted by /trace-mining §5b.

Tests: make test-gap-detect, make test-gap-analysis.

const l = require('~/.claude/scripts/evidence/ledger');
l.append(root, fields) // -> event | null; fills envelope, never throws
l.eventFromHook(hookInput) // -> partial fields | null (unmapped tool)
l.readEvents(root, {days}) // -> events, corrupt lines skipped
l.query(root, {days}) // -> per-item aggregation
l.unused(root, {days, universal}) // -> declared items with zero events
l.summary(root, {days}) // -> fleet-snapshot roll-up | null

Tests: make test-session-ledger (tests/test-session-ledger.js) — schema validation, append (library/CLI/hook mapping), query/unused, backfill marking + idempotency, and the snapshot summary.