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.

Storage

PropertyValue
Location<project-root>/.claude/session-ledger.jsonl
FormatJSON Lines — one event per line, append-only
Schemaschemas/session-ledger.schema.json (validates a single line)
Git statusIgnored — 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).

Event schema

Each line is one event:

FieldTypeMeaning
vintegerEvent format version (currently 1)
tsstringISO 8601 timestamp the event was observed
projectstringBasename of the project root the session ran in
machinestringFleet machine id (hostname-matched against machines.json, falling back to bare hostname)
harnessstringThe agent harness that produced the event (claude-code today)
event_typeenuminvocation (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_typeenumskill · agent · command · task
item_namestringSkill/command slug or agent type; a short task label for no-trigger/improvised
session_idstringHarness session id, when known (may be empty)
sourceenumhook (live collector) · backfill (one-time transcript bootstrap) · observation (a deliberate gap observation, gap-analysis.js observe — #1134)
detailobject?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.

Collector (live mechanism)

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.

Backfill (one-time bootstrap)

bash
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.

Query helpers

bash
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).

Fleet aggregation

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

json
{ "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.

Consumers: gap detection (#1134)

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.

Library API

js
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.