Session-review harness (#131)¶
/session-review mines ground-truth Claude Code session transcripts
(~/.claude/projects/<slug>/*.jsonl) to suggest plugin improvements that reduce
re-work, cut token usage, and improve accuracy.
It fills a blind spot. The plugin already measures quality from two angles. Both have gaps:
/agent-eval+evals/grade agents on a synthetic fixture corpus. This proves an agent can detect a planted issue. It says nothing about real behavior./harness-audit+.claude/metrics/analyze effectiveness from self-reported task logs. These logs are sparse and hold only what the model chose to record about itself.
Neither reads what actually happened: per-turn token usage, tool errors,
failed edits, user corrections, and skill and agent attribution. /session-review
does.
Three stages (the model never reads raw transcripts)¶
| Stage | Component | What it does |
|---|---|---|
| 1. Extract | ${CLAUDE_PLUGIN_ROOT}/scripts/session_report.py --profile maintainer (#127, #2046) |
Deterministic, zero model tokens. Distills MBs of JSONL into a KB digest capturing all four signal classes equally (token / rework / accuracy / utilization). Privacy: metrics only — never prompt or code content. |
| 2. Analyze | agents/session-analysis.md + skills/session-review/SKILL.md (#128) |
A focused agent reads only the digest and maps aggregated patterns to probable plugin causes. |
| 3. Suggest | .dev-team-reports/session-review-<date>.md (#128) |
Ranked recommendations, each tagged {token \| rework \| accuracy}, naming the target artifact and handing off — never auto-applying. |
Hand-off, not auto-apply¶
| Suggestion | Handed to |
|---|---|
| Config / prompt / convention fix | /feedback-learning |
| Model/effort re-tuning | /harness-audit + the agent's model:/effort: frontmatter (ADR 0026) |
| New / changed detection rule | /agent-eval |
| Token-heavy skill / agent | token-efficiency-review |
Trend persistence (#129)¶
Each run appends one metrics-only record to the append-only trend stream
metrics/session-digest.jsonl. The path is deliberately left bare, because /session-review's own
scratch-state writer is out of scope for the #1406 .claude/-scoped artifact
migration. This stream is the real-session counterpart to the self-reported
.claude/metrics/*-task-log.jsonl streams. It lets /harness-audit
consume ground-truth data alongside the task logs. This section is the canonical
description of both the record schema and the harness-audit join.
eval-system.md links here.
Record schema (session-digest/v2)¶
Each line is a JSON object with aggregate counts only — no file names, prompts, command strings, or code (privacy by construction):
| Field | Meaning |
|---|---|
recorded_at |
UTC ISO-8601 of the run (the only wall-clock field) |
sessions |
distinct sessions covered (subagents share their parent's session, so they do not inflate it) |
transcripts / subagent_transcripts |
main-thread sessions vs dispatched agent runs |
tokens |
input/output/cache token totals |
cost_usd, cache_hit_ratio |
session cost and cache-read efficiency |
token.by_agent_type |
per-agent token buckets keyed by agent name — main, unattributed where none resolves, sidechain for an older harness's inlined turns. Was a bare message count before #2010, which read as a token figure under this key and was off from token.totals by orders of magnitude |
rework |
counts: failed_edits, repeated_file_edits, retried_bash_commands, repeated_verify_runs, permission_denials, compaction_events |
accuracy |
tool_calls, tool_error_rate, user_correction_turns |
utilization |
skills_invoked, agents_invoked (RUNS), agent_dispatches (Agent/Task calls), never_observed_skills, never_observed_agents |
v1 records are not comparable to v2 (#1994). Before v2 the extractor
globbed only <project>/<sessionId>.jsonl. Every dispatched agent's own
transcript went unread, so its tokens, tool calls, and rework were missing
entirely. On the machine that motivated this change, the gap was about a third of the tokens and
nearly half the cost. retried_bash_commands and repeated_verify_runs also
changed basis. The extractor now counts them within one thread of execution rather than
per session. Subagents share their parent's sessionId, and a
session-keyed tally scored a review panel's siblings running one command each
as retries. A trend stream holding both eras must split them on schema.
harness-audit consumption (the join)¶
/harness-audit historically read only the self-reported
.claude/metrics/*-task-log.jsonl. It now joins real-session data by reading
metrics/session-digest.jsonl:
- token and cost trends → corroborate or contradict self-reported efficiency claims. The audit's blind spot was that it saw only self-reports.
utilization.never_observed_*→ flag stale or undiscoverable harness surface for the simplification recommendations harness-audit already makes.rework/accuracytrends → evidence for re-tiering or prompt fixes.
Join key: correlate by recorded_at time window. The two streams live at
different roots: metrics/session-digest.jsonl is deliberately bare, and
.claude/metrics/*-task-log.jsonl is migrated (see the note above). The
session-digest stream is ground truth. The task-log stream is self-reported.
Where they disagree, prefer the session digest.
Downstream extraction (no monorepo checkout)¶
/session-review's core steps (Extract, Analyze, and Suggest above) now run from
any installed plugin. session_report.py --profile maintainer ships
inside the plugin package, closing #1779 at the root (#2046/#2047). Only
two OPT-IN paths still require this monorepo's own dev checkout:
--cross-machine sync and rollup, and the raw-log semantic tier it gates. Both are
deliberately self-referential to this marketplace repo's own cross-machine
telemetry database (ADR 0032 Category 2). A downstream
install has no such database.
A downstream user of the plugin may have no access to this repo. That user may want to hand the maintainer their own session data for analysis without
running /session-review's own orchestrated flow. For this case, use the sibling
--profile downstream: ${CLAUDE_PLUGIN_ROOT}/scripts/session_report.py
--profile downstream. It ships inside the plugin package, so it is present
after a normal claude plugin install. It runs from a bare python3 with no
dependencies. It writes ONE metrics-only JSON file for the current
project, an explicit --project <path>, or --all-projects for every
project the plugin has been used in on that machine. The privacy stance matches
everything else in this doc: counts, ratios, and names only, never prompt text,
code, or command strings. The user sends the resulting file to the
maintainer (for example, over MS Teams). The script has no network code
and never transmits anything on its own.
Report schema (downstream-session-report/v4)¶
Alongside the main-thread session at <project>/<sessionId>.jsonl, every
dispatched agent writes its own transcript under
<project>/<sessionId>/subagents/ (a Workflow's agents nest one level deeper
still). The extractor reads both. Two fields distinguish the two signals a reader will
otherwise conflate:
--plugin-version VERSION and its coverage (#2018). This flag scopes the report
to sessions whose project recorded VERSION in its own
.claude/metrics/boundary-events.jsonl. Attribution is best-effort. A session that never
dispatched anything through a hook that stamps session_id cannot be
attributed, so the report excludes it. The report does not drop those sessions silently.
Its top-level version_filter_coverage field (non-null only when
--plugin-version was passed) names requested_version,
sessions_considered, sessions_attributed,
sessions_attributed_other_version (a resolvable version, but not the
requested one; this is the filter working as intended, not a data gap), and
sessions_unattributed (no resolvable version at all). See
knowledge/telemetry-schema.md's "Version-filtered downstream report
coverage" note for the full contract. The exclusion behavior itself is
unchanged. Only its visibility is new.
| Field | Meaning |
|---|---|
transcripts / subagent_transcripts |
main-thread sessions vs dispatched agent runs, both scoped to the reported window |
token.by_agent_type |
per-agent token buckets (#2010), keyed by agent name — main for the main thread, unattributed where no agent is resolvable. Same vocabulary as cost-metering's by_agent_type (knowledge/telemetry-schema.md) and now the same field names as its buckets; deliberately NOT by_subagent, which means main-vs-sidechain in the maintainer profile |
Each token.by_agent_type bucket carries:
| Key | Meaning |
|---|---|
input_tokens, cache_read_input_tokens, cache_creation_input_tokens |
the usage fields that make up what a dispatch carried in |
output_tokens |
what it generated — tracked, but deliberately outside context_tokens |
context_tokens |
the sum of the three context fields. Session telemetry puts ~90% of spend here, which is why this is the figure a panel-cost decision reads |
messages |
assistant messages carrying usage — the value this key held on its own before #2010 |
dispatches |
runs, counted one per subagent transcript. Never inferred from message volume, which would make a verbose agent look cheap per dispatch |
context_per_dispatch |
context_tokens / dispatches, or null when dispatches is 0 (main, and any agent that never ran). Null rather than 0 so a never-dispatched agent cannot sort as the cheapest row |
Reconciliation invariant. The per-agent context_tokens sum exactly to token.totals' three context fields. Both come from the same usage records, so a mismatch means a dispatch was double-counted or dropped. tests/scripts/test_extract_session_report.py pins this invariant.
Not comparable across the #2010 boundary. A pre-#2010 digest carries an int here. The cross-project merge preserves such a label at zero. It does not sum a message count into a token total.
| utilization.agents_invoked | agent RUNS, from each subagent transcript's attributionAgent — ground truth |
| utilization.agent_dispatches | Agent/Task tool calls, i.e. dispatches requested |
The extractor recognizes transcripts by DEPTH. Any .jsonl directly in a project
directory is a main-thread session whatever its name, while below
subagents/ only agent-<id>.jsonl counts.
The harness writes bookkeeping alongside them (subagents/workflows/<runId>/journal.jsonl).
This file is not a transcript, so the extractor skips it. A Workflow's agents carry
attributionAgent: "workflow-subagent", a harness role rather than an agent name.
Their tokens count, but they land in unattributed rather than inventing an agent.
Every string that becomes a report key passes a strict name filter. The extractor aggregates anything
that fails the filter under other. Report keys come from transcripts this
script does not author. A cloned repo's own .claude/agents/*.md chooses
attributionAgent. The script therefore enforces the "names, never full paths" guarantee at the
output boundary rather than trusting each input site.
rework answers at two scopes, deliberately. retried_bash_commands and
repeated_verify_runs are per thread of execution (one transcript). repeated_file_edits, failed_edits, permission_denials, and compaction_events
remain project-wide. A bash retry is a property of one agent's loop. A file is
shared state.
Runs and dispatches legitimately differ. A dispatch made from inside another
agent appears only in that agent's own transcript. A dispatch whose
transcript is absent never ran. agents_invoked falls back to dispatch counts
for a tree written by an older harness that produced no subagent transcripts.
v1 reports are not comparable to v2. Before v2 (issue #1990) the extractor
globbed only the main-thread layout. Subagent tokens, tool calls, and runs
were missing entirely. On the report that surfaced the bug, the gap was 41% of total spend.
retried_bash_commands and repeated_verify_runs also changed basis in v2.
They still carry the v1 (project-wide, session-keyed) basis in session-digest/v1
above. The same names are not comparable across the two artifacts until #1994
lands.
The extractor now counts them within one thread of execution rather than across a whole
project. Subagents share their parent's sessionId, and a session-keyed
tally scores a review panel's siblings running one command each as retries.
OSS complements (#130)¶
For continuous quantitative monitoring, use ccusage, native
OpenTelemetry, or claude-code-log. They cover what /session-review does not.
/session-review covers the plugin-specific qualitative suggestions they
cannot make, because they do not know this plugin's agents and skills. See
session-review-oss-complements.md.
Child issues¶
-
127 — deterministic session-log extractor (now
session_report.py --profile maintainer, #2046)¶ -
128 —
/session-reviewskill +session-analysisagent + report¶ -
129 — trend digest persistence + harness-audit consumption¶
-
130 — document OSS complements¶
-
1990 — count subagent transcripts (
downstream-session-report/v2)¶