Appearance
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
| 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).
Event schema
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.
Collector (live mechanism)
hooks/session-ledger-append.sh — a PostToolUse hook with matcher Skill|Task|Agent, wired in the canonical settings.json:
Skillcalls append asitem_type: skill(tool_input.skill)Task/Agentcalls append asitem_type: agent(tool_input.subagent_type, defaulting togeneral-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/projectsMines 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;
--applywrites. - Idempotent — an event whose
(ts, session_id, item_type, item_name)already exists with sourcebackfillis never appended twice; re-running is safe. - Non-fatal gaps — a transcript whose
cwdno 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.jsonfallback — the same resolutioncdprovapplies).--universalwidens 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 gaps —
scripts/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-homechain), emits thecdprov add <kind>/<name> --nowfix with the automation-level action (L1 ask / L2 confirm / L3+ auto). Exit 0 = no gap, 3 = gap, 2 = usage. - Authoring gaps —
scripts/evidence/gap-analysis.js:observeappendsimprovised/no-triggerevents (sourceobservation);analyzeapplies the recurrence threshold (_workflow.gap_recurrence_countdefault 3 across ≥2 sessions) with cross-project routing (≥2 projects → registry candidate, else project-docs);recommend [--apply]files the qualifying batch asauthoring-gapissues routed toward skill-creator;dismisssuppresses 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 | nullTests: 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.