---
name: readthru-chapters
description: Generate intent-aware, graph-ordered Readthru chapters for the current local git branch and open them in a browser for review. Uses built-in LSP-based graph analysis to order chapters by real call-graph dependencies and surface blast-radius questions; narrates chapters in parallel with cross-context verification on large diffs and machine-validates hunk coverage before display.
user-invocable: true
---

# readthru-chapters

Generates a Readthru chapter run for the current local git branch and opens it in a browser. Uses `readthru prep` to compute the diff, then generates chapters and a prologue, and hands the result to `readthru show` to launch the SPA.

> **Agent-portable.** This skill is written for any capable coding agent (Claude Code, Codex, …), not just one. Where it names a tool (Read/Grep/Glob) or a parallelism primitive (the Task tool), use your agent's equivalent — and where you have no equivalent for subagent parallelism, the sequential fallback (noted in Step 3) produces the identical result, just slower. The `readthru` CLI itself is agent-agnostic.

## Prerequisites

Run these checks before any other work.

1. **`readthru` is installed — auto-install it if not.** Run `which readthru`. If it exits
   non-zero, install it for the user without making them do it manually: print one line
   (`Installing readthru…`), run `npm install -g readthru`, then re-run `which readthru`.
   - If it now resolves, continue silently.
   - If the install fails (e.g. npm not on PATH, permission error), stop and show the
     underlying error plus: `Could not auto-install readthru. Install it manually with
     'npm install -g readthru' and retry.` Do not continue.

2. **The current directory is a git repo.** Run `git rev-parse --is-inside-work-tree`. If it does not print `true`, stop with:

   ```
   /readthru-chapters must be run inside a git repository.
   ```

## Step 0 — Intent

Before running prep, establish the INTENT line: one sentence describing what this branch/release is meant to do.

- If the user has already stated the purpose (in this request or earlier), use that.
- Otherwise, ask **exactly one** question and wait for the answer: `One line: what is this branch/release meant to do?`

**Validate the intent against the commit signal.** After Step 1, prep emits an `=== INTENT ===` section derived from the commit log (ticket prefixes, commit types, a release-merge flag). If the user's answer was vague or empty — a generic phrase like "more development", "updates", "wip", or one or two non-specific words — do **not** proceed on it: propose a concrete candidate built from those ticket prefixes + commit types and ask the user to confirm or correct it (this one extra question is worth it; a junk intent silently degrades clustering and finding severity). If `=== INTENT ===` says **RELEASE-MERGE LIKELY**, treat the change as a bundle of independent tickets — chapter into thematic buckets (one per ticket/feature), relax the 8–12 budget, and say so in the prologue rather than forcing a single arc.

Carry the resulting INTENT line through the rest of the workflow: inject it into clustering, into every narrator prompt, and into the prologue. Reviewer accuracy improves substantially when the change's purpose is known — the intent is what lets clustering and narration distinguish load-bearing changes from incidental ones, so do not skip it.

## Step 1 — Run prep

```bash
PREP_FILE=$(readthru prep)
```

`readthru prep` auto-detects the base ref (main/master), computes the merge-base, generates the diff, filters out lockfiles/binaries, and formats hunks with line numbers for analysis. By default it auto-detects the diff scope: if uncommitted changes are present the diff includes staged, unstaged, and untracked files; otherwise it uses the committed branch diff. It writes a plain-text file and prints only the file path to stdout.

**Cache check (do this first).** If the prep file opens with a `=== CACHED RUN ===` section, this exact diff was already reviewed. Do **not** regenerate — tell the user it's cached (cite the runId) and run `readthru open` so they can view the existing run. Skipping generation here is the point: the same diff yields the same review every time, for free.

`prep` and `show` also accept positional git refs:

```bash
PREP_FILE=$(readthru prep main)
PREP_FILE=$(readthru prep main feature)
PREP_FILE=$(readthru prep main..feature)
PREP_FILE=$(readthru prep main...feature)
```

Use the same positional refs for `show`:

```bash
readthru show "$AGENT_OUTPUT" main..feature
```

Both `prep` and `show` accept these optional flags:

- **`--base <ref>`** — base ref to diff against (default: auto-detect main/master).
- **`--compare <ref>`** — compare ref to diff against `--base`.
- **`--ref <mode>`** — diff scope. One of:
  - `work` — staged + unstaged + untracked changes (full working tree vs merge-base).
  - `staged` — only staged changes (index vs HEAD).
  - `unstaged` — only unstaged changes (working tree vs index).
  - Omitted — auto-detect (equivalent to `work` when uncommitted changes exist, committed branch diff otherwise).

When flags or positional refs are specified, pass the same scope to **both** `prep` and `show`:

```bash
PREP_FILE=$(readthru prep --base feature-a --ref staged)
# ... later ...
readthru show --base feature-a --ref staged "$AGENT_OUTPUT"

PREP_FILE=$(readthru prep --base main --compare feature)
# ... later ...
readthru show --base main --compare feature "$AGENT_OUTPUT"
```

If `prep` exits non-zero, relay its stderr to the user and stop.

**Do not modify files in the working tree between running `prep` and running `show`.** Both commands independently snapshot the git state. If the diff changes between them, `show` will reject the chapters with a hunk coverage error because the hunks no longer match.

## Step 2 — Read prep output

Read `$PREP_FILE` via the Read tool (or equivalent). For large diffs, use the Read tool's `offset` and `limit` parameters to read in chunks.

The file has these sections, separated by headers, in this order:

1. **`=== COMMIT MESSAGES ===`** — `git log --oneline` output for prologue context.
2. **`=== GRAPH CONTEXT ===`** — optional; present only when graph analysis succeeds (see Step 2.5).
3. **`=== COMPLETENESS ===`** — optional; grounded, deterministic completeness findings over parsed infra facts (see Step 2.6).
4. **`=== HUNK INDEX ===`** — a compact manifest of every hunk (see below). Read this first to size the diff and plan clustering.
5. **`=== HUNKS ===`** — formatted diff hunks with line numbers. Each hunk looks like:

```
=== File: src/app.ts (modified) | filePath: "src/app.ts", oldStart: 1 ===
=== Hunk @1: @@ -1,5 +1,6 @@ ===
1 1 | const a = 1;
2   |-const b = 2;
  2 |+const b = 3;
  3 |+const c = 4;
3 4 | const d = 5;
```

The two number columns are the **old line number** (left) and **new line number** (right). A blank column means the line doesn't exist on that side — additions have no old line number, deletions have no new line number. These numbers are used directly for `lineRefs` in key changes (see Step 3d).

### HUNK INDEX

The `=== HUNK INDEX ===` section is a compact manifest of every hunk in the diff:

```
<N> hunks across <M> files
src/app.ts | modified | oldStart 1 | +3/-1
src/util.ts | added | oldStart 0 | +20/-0
```

- **Header line:** `<N> hunks across <M> files`. Read `<N>` to choose the mode in Step 3.
- **Per-hunk line:** `<path> | <status> | oldStart <N> | +<added>/-<deleted>`. The `(filePath, oldStart)` pair on each line is exactly what `hunkRefs` entries reference, so the index is the authoritative checklist for hunk coverage and for assigning hunks to chapters.

## Step 2.5 — Graph context (built-in, optional)

`readthru prep` automatically runs LSP-based graph analysis and may embed a `=== GRAPH CONTEXT ===` section between COMMIT MESSAGES and HUNKS in the prep file. The section is present only when analysis succeeds and finds changed symbols; it is silently absent on unsupported languages, missing language servers, errors, or timeout (30 s). Never block the review on its presence.

**Section shape:**

```
=== GRAPH CONTEXT ===
risk: <low|medium|high|critical> — <N> files outside this diff depend on <M> changed symbols

change dependencies (callee → caller):
  <calleeName> (<file>) → <callerName> (<file>:<line>)

<symbol name> (<kind>, <file>:<startLine>-<endLine>) [needs attention] [signature changed] [on a live path]
  <N> files depend on this and weren't updated:
      <file>:<line>  [<kind>]  <referencing source line>
  entry paths:
    <entrySymbol> (<file>) → <hop> → <symbol>
  callees: <name>, <name>, ...

<symbol name> (<kind>, <file>:<startLine>-<endLine>) [safe]
```

- **Risk line:** overall blast-radius severity for the whole diff.
- **`change dependencies (callee → caller):` block:** present only when changed symbols call each other. Each line `calleeName (file) → callerName (file:line)` is one edge between two symbols **both changed in this diff** — it means the caller's code depends on the callee's change. The callee's chapter should therefore come **before** the caller's chapter (and connected symbols usually belong in the same or adjacent chapters).
- **Per-symbol block:** one block per analyzed changed symbol. The header carries an **attention tag** — `[needs attention]`, `[uncertain]`, or `[safe]` — plus optional `[signature changed]` (the symbol's API changed) and `[on a live path]` (it is reachable from a real, non-test entry point). For non-safe symbols, the block then lists the **not-updated dependent files**: files that reference this symbol but were NOT themselves changed in this diff, each line `file:line  [kind]  referencing source line` where `kind` is `import` / `jsx` (renders) / `call` / `type` / `other` — telling you HOW the dependent uses the symbol. **Safe** symbols print only the one quiet header line (every dependent was updated, or none exist, or the change is low-risk). Entry paths trace root→symbol through the call graph (a chain of length 1 means the symbol is itself a graph root). Callees are symbols this one calls.

When the section is present, feed it into Steps 3 and 4:

- **Clustering (3a):** call-graph edges give true dependency order between chapters (callee chapters before caller chapters).
- **Narration (3c):** summaries may state blast radius plainly (e.g., "`evaluate()` has 3 dependent files outside this diff that weren't updated").
- **Key changes (3d) — blast-radius verdicts:** For each changed symbol the GRAPH CONTEXT marks `[needs attention]` or `[uncertain]` with not-updated dependent files: judge whether each dependent must change, using the referencing-line snippet as evidence. Emit a key-change question ONLY when a dependent plausibly needs updating — frame it with the verdict and the evidence: e.g. "`CallDetail` gained a required `groupId` prop; `PageA` (src/pages/PageA.tsx:2) renders `<CallDetail id=...>` without it — update needed?". Do NOT emit a question for `[safe]` symbols or when the change is additive/compatible for a dependent (e.g. a new optional prop, an added function with no existing callers). Blast-radius questions must clear the same bar as all key changes (3d): real human judgment, evidence-backed, no noise. These graph-derived questions are in ADDITION to the normal product-judgment key changes; both kinds coexist.
- **Prologue (Step 4):** focusAreas severity should reflect the risk line; symbols with not-updated dependents and no corresponding test changes are candidates for `testing-gap` focus areas.

For chapter anchors that need deeper graph context while narrating, the agent MAY run:

```bash
readthru graph <symbol> [--depth <1|2>] [--json]
```

`--depth 1` returns direct callers only; `--depth 2` (default) also returns callers of callers. `--json` outputs machine-readable JSON. This command is optional and bounded — use it when the prep section leaves a specific symbol's blast radius unclear.

## Step 2.6 — Completeness context (built-in, grounded)

`readthru prep` may embed a `=== COMPLETENESS ===` section between GRAPH CONTEXT and HUNK INDEX. Its findings are **GROUNDED**: each is produced by a deterministic rule over infra facts the engine actually parsed (helm values files across every environment, plus datastore-client and env-var reads in the changed code). There is **no LLM in this section** — if a finding is here, it fired off parsed facts, not judgment.

Each line is shaped `[ruleId] title — detail (locations)`. The current rules:

- **`datastore-not-provisioned`** — a datastore is used in the changed code but provisioned in NO environment the engine parsed.
- **`env-parity`** — a datastore is provisioned in some environments but absent from others (e.g. `clickhouse provisioned in dev, staging — absent from prod, uat`). This is the classic "shipped to dev, forgot prod" omission. A datastore declared once in the base (`default`) values is treated as inherited by every overlay, so it never triggers a false parity gap.
- **`env-read-undefined`** — an env var is read in the changed code but not found in any parsed helm values. Only fires when the repo declares ≥1 env var inline in its values (evidence it sources config that way); vars sourced via `envFrom`/secrets/CI are invisible to this check, so the finding is honestly scoped ("verify manually if you use those") rather than claiming a confirmed gap.
- **`model-without-migration`** — the change declares a NEW ORM model/table (Django, SQLAlchemy/Alembic, Drizzle, Prisma, TypeORM, Sequelize, Rails) but adds no migration file for that framework. A brand-new table with no migration ships code expecting a schema no environment will have. Self-grounding (the model declaration IS the evidence) and honestly hedged — if the repo generates/applies migrations in a separate step or repo, the finding tells you to verify that path. It detects new *declarations*, not column-level edits to existing models, and never covers Go ORMs / runtime auto-migration (the coverage line names this).
- **`runtime-incompatible-api`** — the changed code uses a stdlib API removed in the Python version the project declares it targets (`requires-python` / `.python-version` / `setup.cfg`). A pure "beyond the diff" catch — the changed lines look fine; the break only exists once you cross them with the declared runtime. Fires only when the removal version is at or below the project's declared FLOOR (every supported runtime already lacks it → a certain break), and only when a target is declared. Example: `import imp` in a project with `requires-python >=3.12` (`imp` was removed in 3.12).

The section always ends with a **fail-loud `coverage:` line** stating what was parsed and, explicitly, what the engine NEVER checks (terraform, k8s manifests, docker-compose, dockerfile; Go ORMs / runtime auto-migration for the migration rule; and the runtime target it did or did not check). A finding-free section still emits this line — silence is not "all clear", it is "the rules that ran found nothing".

### `=== MIGRATION PARITY ===` — a HINT, not a grounded finding

`readthru prep` may also embed a `=== MIGRATION PARITY ===` section when it detects a cross-language migration that **drops an ORM** (the removed side used an ORM in language A; the added side does raw DB access in a different language B with no ORM). Unlike the grounded rules above, this is a **hint for your judgment**, not a proven gap — cross-language semantic equivalence is not something to assert deterministically. The detector found the *shape*; you decide whether the drop is intentional. Act on it in the Step 3.6 completeness pass: produce an **inferred** `completeness-gap` ("is dropping ORM X intentional? it silently loses soft-deletes/hooks/validation/migration-tracking — confirm the rewrite re-implements what's relied on") **unless** the added code clearly re-implements those capabilities. Never mark it `grounded`.

**TRUST these findings and present them.** In Step 3.6 / Step 4, every `=== COMPLETENESS ===` finding becomes a `completeness-gap` focus area with **`provenance: "grounded"`** (see Step 4 focusAreas). They are distinct from — and additive to — the gaps you infer yourself in the Step 3.6 pass (which stay `provenance: "inferred"`). The engine only covers the domains in its coverage line, so you must STILL run your own inferred completeness pass for everything it does not parse.

## Step 3 — Cluster + narrate

Produce a `chapters` array. Each chapter groups related hunks into a coherent story beat, narrates them for a reviewer unfamiliar with this part of the codebase, and flags judgment calls that need human input.

### Mode selection

Read the hunk count `<N>` from the `=== HUNK INDEX ===` header line.

- **`<N>` ≤ 60 → SINGLE-AGENT MODE.** Do the current flow yourself: read every hunk, cluster (3a), and narrate (3c/3d/3e) — then run the cross-context verification pass **3-P3** on the assembled chapters, and assemble per **3-P4**.
- **`<N>` > 60 → PIPELINE MODE.** Run the orchestrated pipeline below (**3-P1 → 3-P2 → 3-P3 → 3-P4**). You act as the orchestrator and spawn subagents for narration and verification.

The clustering rules (3a), narration rules (3c), key-change rules (3d), and output format (3e) below are shared by both modes; the pipeline steps reference them.

### 3-P1 — Clustering (orchestrator, PIPELINE MODE)

Cluster from the `=== HUNK INDEX ===`, `=== GRAPH CONTEXT ===`, and `=== CANDIDATE CHAPTERS ===` sections alone — do **not** read every hunk's content yet.

- **Start from the candidate partition.** When prep emits `=== CANDIDATE CHAPTERS ===`, adopt it as your starting grouping — it is the deterministic, graph-derived skeleton (connected components of the change-dependency graph, with orphan hunks bucketed by directory), so the same diff produces the same boundaries every time. Refine it: merge thin buckets, split an incoherent one, rename for clarity — but treat a deviation as a deliberate, reasoned move, not a fresh clustering from a flat list. Every hunk in the candidate set must still land in exactly one chapter.
- **Grouping:** apply the 3a rules. Use the change-dependency edges as the primary grouping signal — symbols connected by an edge belong in the same chapter or in adjacent chapters.
- **Ordering:** put callee chapters before caller chapters (per the edges). Where no edge exists, fall back to foundation → core → integration.
- **Ambiguity:** when the index alone leaves grouping ambiguous — generic filenames, `utils`, shared modules, or files touched by unrelated concerns — READ those hunks' content (from `=== HUNKS ===`) before deciding. Filename-only clustering of ambiguous hunks is forbidden.

**Chapter count budget:** every chapter costs two subagents (narrator + verifier), and wall-clock scales with chapter count. Target **8–12 chapters** even on very large diffs: merge thin adjacent beats (housekeeping, seed tooling, doc-only groups) into their nearest feature chapter or one combined chapter rather than giving each its own. Only exceed 12 when chapters would otherwise lose coherence — and on a **RELEASE-MERGE LIKELY** diff (per `=== INTENT ===`), do exceed it: one chapter per ticket/feature bucket reads far better than forcing unrelated tickets together, since there is no single arc to find.

**Output:** a chapter plan — for each chapter, a title plus its assigned hunk list as `(filePath, oldStart)` tuples. Every hunk from the HUNK INDEX must appear in exactly one chapter (3b).

### 3-P2 — Parallel narration (subagents, PIPELINE MODE)

Spawn one narrator subagent per chapter, **up to 8 concurrent**, using your agent's parallel-subagent mechanism (in Claude Code: the Task tool with multiple calls in a single message; in Codex or others: their equivalent spawn). **If your agent has no subagent/parallelism primitive, fall back to narrating the chapters sequentially yourself** — the chapter plan, rules, and verification are identical; only the wall-clock changes.

**No phase barrier — interleave verification (this is the wall-clock budget mechanism).** Do NOT wait for all narrators before starting verifiers. As each narrator batch returns, launch that batch's verifiers (3-P3) **in the same message** as the next narrator batch. Example with 10 chapters: message 1 = narrators 1–8; message 2 = verifiers for whichever of 1–8 returned + narrators 9–10; message 3 = remaining verifiers + any fix-recheck agents. Total wall-clock target for the whole pipeline: **under ~15 minutes**.

Each narrator prompt MUST contain:

- The **INTENT line** from Step 0.
- The chapter **title** and its assigned hunks' **FULL text** — either copied verbatim from the prep file, or given as exact `sed -n '<start>,<end>p' "$PREP_FILE"` line ranges for the narrator to read.
- The relevant `=== GRAPH CONTEXT ===` blocks for the chapter's symbols (per-symbol blocks with their attention tags and not-updated dependent files + any change-dependency edges that touch them).
- The **3c narration rules and 3d key-change rules verbatim** — including the blast-radius verdict guidance in 3d (narrators run in fresh contexts, so the verdict rules must arrive inside the 3d text they are handed).
- This prohibition list, **verbatim**:
  - Do NOT flag anything a linter, type checker, or CI would catch.
  - Do NOT raise theoretical risks ("could be slow if...") without evidence in the hunks.
  - Do NOT comment on unchanged code.
  - Do NOT invent questions to fill keyChanges — empty is correct when nothing needs human judgment.
  - Every factual claim must be checkable against the hunk text or graph data you were given.

Narrators MAY investigate beyond their prompt: read surrounding repo files and run `readthru graph <symbol> --json` for blast-radius evidence — **budget: at most 3 investigation tool calls**, and only to substantiate a specific claim or key-change question the narrator is about to make. Narration must come from the provided hunks; investigation is for evidence, not exploration. (Include this budget line in the narrator prompt.)

**Output contract:** the narrator returns the chapter JSON object only (3e schema), with `hunkRefs` exactly matching its assignment — no extra, no missing.

### 3-P3 — Cross-context verification (subagents, BOTH MODES)

**Tiering — not every chapter needs an agent.** A chapter qualifies for the machine-only tier when its `keyChanges` is empty AND its summary makes no blast-radius/caller/graph claims (pure mechanical content: config mirrors, renames, generated files, housekeeping). For those, the orchestrator verifies mechanically (hunkRefs exactly equal the assignment — already scriptable) and skips the verifier subagent. Every other chapter gets a full verifier.

For each qualifying chapter — single-agent and pipeline alike — spawn a **verifier** subagent. The verifier MUST NOT be the narrator and MUST receive **no narrator reasoning** — only the chapter JSON plus the same hunk text (and graph blocks) the narrator had. Verifiers run interleaved with narrator batches per 3-P2 — never as a separate phase after all narration.

The verifier checks:

1. Every `lineRef` points at the cited content — correct file, correct `side`, and `startLine`/`endLine` that actually exist in the hunk on that side.
2. Every factual claim in `summary` and `keyChanges` is supported by the provided hunks/graph data. Unsupported claims are **REMOVED, not softened**.
3. Prohibition-list compliance (the five rules above).
4. `hunkRefs` exactly equal the assignment.

The verifier returns **PASS** or a **fix list**.

- Fix list → the narrator (or the orchestrator in single-agent mode) applies the fixes → the verifier re-checks **once**.
- Still failing after the re-check → the orchestrator **rewrites that chapter conservatively**: narration from the hunks only, and `keyChanges` only where the evidence is unambiguous.

### 3-P4 — Assembly (orchestrator, BOTH MODES)

Merge the chapter JSONs in the planned order. Dedupe near-identical `keyChanges` across chapters — keep the one with the tighter `lineRef` and drop the rest. Renumber `order` fields to be contiguous and 1-indexed.

### 3a — Clustering rules

Group hunks by **causal relationship** — changes that set up or enable later changes belong together.

- Spanning multiple files is expected and correct (e.g., schema + API + UI for one feature = one chapter).
- Moves and refactors are a single chapter — when code is removed from one file and added to another (or a file is deleted and a similar one created), group the deletion and addition hunks together as one "Move/Refactor" chapter, not separate "Remove" and "Add" chapters.
- Split only when changes are truly independent — a reviewer could understand one without knowing about the other.
- Tests belong with their implementation chapter.
- Config/dependency changes can be their own chapter if unrelated to a feature chapter.

**Chapter ordering:**

1. Foundation first: types, interfaces, schemas, utilities that others depend on
2. Core logic next: main implementation
3. Integration last: wiring, configuration, tests

Consider symbol dependencies between chapters — a chapter that introduces a type another chapter uses must come first. When graph context is available (Step 2.5), derive these dependencies from actual call-graph edges rather than guessing, and prefer grouping hunks whose symbols share an execution flow.

**Hunk ordering within a chapter:**

- Group all hunks from the same file together — do not interleave hunks from different files.
- Within the same file, list hunks in ascending `oldStart` order (matching file layout).

### 3b — Self-validation rules

Every hunk in the formatted diff **must** appear in exactly one chapter. No hunk may be omitted and no hunk may appear in more than one chapter.

Each hunk header in the prep output has the format:
```
=== File: <path> (<status>) | filePath: "<path>", oldStart: <N> ===
```

Use the `filePath` and `oldStart` values from these headers to build `hunkRefs`.

`readthru show` validates hunk coverage automatically — it will error with a list of missing or extra hunks if the chapters don't account for every hunk in the diff. If this happens, fix the chapters and retry.

### 3c — Narration rules

Write each chapter as a story beat — a meaningful step that moves the branch forward, not a summary of files changed.

- **Title:** action-oriented verb phrase, max 8 words (e.g., "Wire org ID through the API layer"). No filler like "Add support for".
- **Summary:** 2–3 sentences covering what this chapter enables and why. Lead with impact, then connect to the broader purpose. When a chapter builds on a previous one, open with that causal link explicitly (e.g., "Now that X is in place…").
  - Keep paragraphs short. Prefer splitting distinct points into separate short paragraphs (separated by a blank line) rather than writing one long dense paragraph. Each paragraph should convey a single idea.
  - Markdown allowed: `**bold**` for emphasis, `*italics*` for nuance, `` `backticks` `` for inline code references, and fenced code blocks when a short snippet (≤ 6 lines) helps illustrate the change.

### 3d — Key change rules

Key changes are **judgment calls only a human reviewer can make** — things that require product context, team conventions, or knowledge of the author's intent. Linters, type checkers, and code-review bots already cover correctness and style; skip anything they can catch. Ignore auto-generated files.

Return an **empty array** when nothing needs human input — do **not** invent items to fill the list. When a chapter is a straightforward rename, type fix, or mechanical refactor with no judgment calls, `keyChanges` should be `[]`.

Frame each item as a **question**.

Each key change includes `lineRefs`: one line range per distinct spot the question depends on. Most questions touch a single location, so use one range; only add more when the judgment genuinely spans related code in different places.

**Blast-radius verdict questions (graph-informed).** For each changed symbol the GRAPH CONTEXT marks `[needs attention]` or `[uncertain]` with not-updated dependent files: judge whether each dependent must change, using the referencing-line snippet as evidence. Emit a key-change question ONLY when a dependent plausibly needs updating — frame it with the verdict and the evidence: e.g. "`CallDetail` gained a required `groupId` prop; `PageA` (src/pages/PageA.tsx:2) renders `<CallDetail id=...>` without it — update needed?". Do NOT emit a question for `[safe]` symbols or when the change is additive/compatible for a dependent (e.g. a new optional prop, an added function with no existing callers). Blast-radius questions must clear the same bar as every other key change: real human judgment, evidence-backed, no noise. These graph-derived questions are in ADDITION to the normal product-judgment key changes; both kinds coexist.

**`lineRefs` for a blast-radius question.** The dependent's referencing line lives in a file that is NOT in the diff, so it has no hunk. `readthru show` only accepts `lineRefs` that fall inside an in-diff hunk on the cited side — a `lineRef` pointing at a non-diff dependent file is silently dropped. So for a blast-radius question, put the dependent reference (`file:line` + the snippet) in the question TEXT, and point `lineRefs` at the **changed symbol's own line in the diff** (the line where its signature/shape changed). That keeps the reviewer anchored at the source of the break while the text names the dependent that must follow.

**Reading line numbers from the `=== HUNKS ===` section:** Each diff line shows two number columns — old (left) and new (right). Use these numbers directly:
- For `side: "deletions"` — use the **old** (left) column number as `startLine`/`endLine`.
- For `side: "additions"` — use the **new** (right) column number as `startLine`/`endLine`.
- Do **not** count lines yourself — read the numbers from the formatted output.

Keep ranges tight — point to the specific lines the question is about, not the entire hunk. `startLine` and `endLine` must both be positive integers with `endLine >= startLine`.

**Good examples:**

- "Should `retryCount` reset when the user switches orgs?"
- "`validateUser` gained a required `orgId` parameter; `loginHandler` (src/auth/login.ts:42) still calls `validateUser(name)` without it and wasn't updated — does it need the new argument, or is the default intentional there?" (graph-informed — `lineRefs` point at `validateUser`'s changed signature in the diff)
- "`CallDetail` now requires a `groupId` prop; `DashboardPage` (src/pages/dashboard.tsx:18) renders `<CallDetail id={id} />` without it and wasn't touched — update needed?" (graph-informed)
- "Is a 60-minute session timeout appropriate for this user base, or would 30 minutes be safer?"
- "Does this new index cover the query patterns the team actually uses in production?"

**Bad examples:**

- "Check that the auth logic is correct." — vague, verifiable by reading the code
- "The function now handles errors." — changelog item, not a question
- "Make sure the tests pass." — CI catches this, not a human judgment call

### 3e — Output format

Produce an array of chapter objects. Each chapter:

```jsonc
{
  "id": "chapter-1",    // unique within the run, e.g. "chapter-1", "chapter-2", …
  "order": 1,           // positive integer, 1-indexed
  "title": "Short imperative title",
  "summary": "Why this chapter matters to the reviewer.",
  "hunkRefs": [
    // one entry per hunk in the chapter
    { "filePath": "path/to/file.ts", "oldStart": 42 }
  ],
  "keyChanges": [
    // zero or more judgment-call questions
    {
      "content": "A judgment-call question for the reviewer.",
      "lineRefs": [
        {
          "filePath": "path/to/file.ts",
          "side": "additions",
          "startLine": 50,
          "endLine": 55
        }
      ]
    }
  ]
}
```

- Do **not** invent `hunkRefs` — only use `(filePath, oldStart)` tuples that actually appear in the formatted hunks.
- `keyChanges[].lineRefs` must have at least one entry per key change.

## Step 3.6 — Completeness pass (BOTH MODES)

The diff shows what changed. The most dangerous defects in a large change are **omissions** — the thing the change *implies must also exist* but doesn't, and which no line-by-line review can see because the evidence is a file that isn't in the diff (e.g. code migrated to a new datastore, but no infra provisions it; an env var read in code but defined in no environment's values; a new model with no migration). This pass hunts those, deliberately.

**First, fold in the grounded findings.** If prep emitted a `=== COMPLETENESS ===` section (Step 2.6), TRUST every finding there and turn each into a `completeness-gap` focus area with **`provenance: "grounded"`**, reusing its title/detail/locations. These already fired deterministically off parsed infra — do not re-derive or second-guess them. Then run the inferred pass below for everything the engine's `coverage:` line says it did NOT check; those findings stay `provenance: "inferred"`.

For the change **as a whole**, derive its *implications* and verify each against the **actual repo** (not just the diff) using your agent's file-read and search tools (Read/Grep/Glob in Claude Code; the equivalents in Codex or any other agent). Budget generously here — this is the highest-value pass; do not ration tool calls the way narration does. Check at least these classes when the change touches them:

- **Infra provisioning:** a new datastore / queue / cache / external service introduced in code (a new client/connection, image, or library) → is it provisioned in infra (helm/k8s/terraform/compose), **in every environment**? Multi-env repos (`dev`/`staging`/`uat`/`prod` value files) are where this bites — a datastore added to one env's values but not the others is a prod incident.
- **Config / env parity:** an env var or config key read in code → is it defined in every environment's values / `.env.example`? A key present in some env files but missing in others is a gap.
- **Migrations:** a new persisted model / table / column → is there a corresponding migration? Note a brand-new model/table declaration is often already caught **grounded** by the `model-without-migration` rule (Step 2.6) — do not re-report what that section flagged. Your inferred job here is what the rule cannot see: column/field additions to an *existing* model, Go ORMs / auto-migration repos the rule never checks, and migrations that exist but are wrong/incomplete.
- **Wiring:** a new module / handler / route / worker → is it registered / exported / mounted / scheduled, not just defined?
- **Cross-env / cross-file parity:** a value changed in one of a set of parallel files (per-env configs, per-service charts) → were the siblings updated too?

Emit a `completeness-gap` focus area (Step 4) **only after a real repo search** — never off the diff alone. A gap you CONFIRMED (you searched for the implied artifact and it genuinely is not there) surfaces with the default `confidence: "confirmed"`; a gap you searched for but genuinely **could not determine** still surfaces, with `confidence: "uncertain"` (Step 3.7) — say what you couldn't verify. What you must NOT do is emit a gap you never searched for, or keep one you actively **disproved** (drop those). Two shapes to watch for, both established by repo search, not by the diff:
- **Total omission:** *"Fraud alerts migrated to ClickHouse (chapter 6), but `grep -ri clickhouse infrastructure/` found no provisioning in any helm values — confirm the infra was added before merge."*
- **Partial / env-parity omission (the more common, more dangerous one):** the artifact exists in SOME environments but not all. *"ClickHouse is provisioned in `infrastructure/dev/helm/values.yaml` but `grep` found it absent from staging/uat/prod values, which still provision postgres — the service will fail on startup in those environments. Confirm parity across all envs."* The diff only shows the env that changed; the missing siblings are invisible without searching the repo.

Set `provenance: "inferred"` for gaps YOU find in this pass (your judgment, grounded in a repo search but not a deterministic check). Gaps adopted from the `=== COMPLETENESS ===` section keep `provenance: "grounded"` instead — never downgrade a grounded finding to inferred.

**Fail loud — this is non-negotiable.** End the pass by recording, in the prologue, a one-line **coverage note** of what you actually checked and, explicitly, **what you did NOT verify** (a class you skipped, a domain with no files, infra tooling you don't understand). Put it in the prologue `outcome`/a focus area, or a `completeness-gap` with `severity: "info"` titled "Coverage". The reviewer must never read silence as "all clear." Distinguish a **confirmed gap** (you looked, it's missing) from **could-not-verify** (you couldn't determine) — say which.

## Step 3.7 — Adversarial verification (refute before you surface)

Before any **inferred** finding becomes a focus area, try to **kill it**. A review that cries wolf is worse than one that stays quiet — a single false alarm teaches the reviewer to ignore the next one, and the tool's whole value is trust. So for each candidate inferred finding (every focus area you reasoned out yourself: risk flags, `completeness-gap`s from Step 3.6, architecture/security concerns — NOT the grounded `=== COMPLETENESS ===` findings, which are already deterministic), run one honest refutation pass:

1. **State the finding as a falsifiable claim.** "X is missing / X is unsafe / X breaks Y."
2. **Search for evidence it is WRONG**, with real tool calls (your agent's grep/glob/read equivalents) — do not re-confirm your own hunch, actively look for the disproof: the artifact actually exists elsewhere, the input is already validated upstream, the code path is unreachable/dead, the env var is defined in a file you hadn't checked, the caller already handles the case. **The burden to DROP is proof: a finding is refuted ONLY when you find concrete evidence the concern is unfounded. An inconclusive search is never a refutation — it is `uncertain`.** Search hard for the disproof, but absence of disproof is not disproof.
3. **Assign a verdict** and act on it:
   - **refuted** — you found concrete disproof → **DROP the finding entirely.** It never reaches the prologue. (This is the "if it's sure nothing's affected, I don't want noise" case.)
   - **confirmed** — your refutation attempt failed; the concern survives a genuine search → keep it, `confidence: "confirmed"` (the default; needs the reviewer's attention).
   - **uncertain** — you could neither confirm nor refute within a reasonable search → keep it but set `confidence: "uncertain"` and say in the description WHAT you couldn't determine and why. (This is the "or it's not sure" case — surfaced, never hidden.)
4. **Fail loud about what you killed.** If you dropped one or more findings as refuted, note it in the coverage line ("considered N possible gaps; refuted M — e.g. ‹claim› because ‹evidence›"). A silently-dropped finding looks identical to one never considered; the reviewer should see that you challenged and cleared it.

Grounded findings (Tier 2 `=== COMPLETENESS ===`) skip this pass: they are already `confidence: "confirmed"` by construction — a deterministic check, not a hunch to refute. Budget generously here; this pass is what keeps the signal-to-noise high.

## Step 4 — Generate prologue

Generate the **prologue** AFTER verification (3-P3), assembly (3-P4), and the completeness pass (3.6) — a high-level overview of the entire change that helps reviewers orient themselves before diving into individual chapters. Building it last means it summarizes only verified, evidence-backed chapters, and folds in the completeness findings.

Produce the prologue from the **verified chapter summaries** + the `=== COMMIT MESSAGES ===` section + the **INTENT line** (Step 0). Produce a `prologue` object with the following fields:

### motivation (string or null)

One sentence a non-engineer would understand. What was broken, annoying, or missing — from a person's perspective. If the commit messages are generic and the diff doesn't make the motivation obvious, use `null`.

**Good:** "Dashboards would break during deploys, so people had to keep refreshing until things came back."
**Bad:** "The API client had no retry logic for 503 errors." (too technical — no one outside the team knows what that means)

### outcome (string or null)

One sentence a non-engineer would understand. What's better now. Same null rule as motivation.

**Good:** "Dashboards stay up during deploys now."
**Bad:** "Added exponential backoff with a base delay of 100ms." (implementation detail)

### diagram (string or null)

A Mermaid diagram source string (**without** fenced code block markers) that lets a reviewer grasp the big picture at a glance. Emit one only when the change spans multiple components in a data or control flow — a new endpoint wiring through middleware to a database, a state machine gaining transitions, an event pipeline connecting producers to consumers.

Use `null` for single-file changes, renames, config updates, test-only changes, dependency bumps, or anything where the key changes alone tell the story. **Most changes should NOT have a diagram** — omit it when a diagram wouldn't help.

Pick the diagram type that fits the change:
- `graph TD` or `graph LR` — data flow, component wiring, module dependencies
- `sequenceDiagram` — request/response or call chains across layers
- `stateDiagram-v2` — lifecycle or state machine changes

Keep it concise — under 10 nodes; the diagram renders in a narrow side panel. Quote node labels containing special characters (`@ # < >`): `A["@scope/package"]`, not `A[@scope/package]`.

### keyChanges (array of 2–5 objects)

Each object has:
- `summary`: 6–10 words describing what's different now. **Outcome-focused**, not action-focused.
- `description`: Capitalized sentence, 10–15 words of additional context.

**Good:** `summary: "Audit runs are now tracked in a database"`, `description: "Uses new Drizzle ORM schema with full history retention"`
**Bad:** `summary: "Adds Drizzle ORM layer"` (action-focused — describe what changed, not what you did)

### focusAreas (array of 1–5 objects)

Always provide at least 1 focus area. Even clean changes have spots worth a reviewer's attention.

When graph context is available (Step 2.5), ground focus areas in it: let the risk line inform severity, name symbols with large external caller counts in descriptions, and flag symbols with external callers and no test changes as `testing-gap`.

Each object has:
- `type`: one of `security`, `breaking-change`, `high-complexity`, `data-integrity`, `new-pattern`, `architecture`, `performance`, `testing-gap`, `completeness-gap`
- `severity`: one of `critical`, `high`, `medium` (for problems) or `info` (for points of interest)
- `title`: 3–5 word noun phrase (e.g., "Unvalidated user input")
- `description`: WHY this was flagged + a declarative action for the reviewer. Use "confirm", "verify", or "check" to give the reviewer a specific task.
- `locations`: array of file paths where this applies
- `provenance`: `"grounded"` or `"inferred"` (default `"inferred"`). Set `"grounded"` only for findings adopted from the `=== COMPLETENESS ===` section — deterministic checks over parsed infra facts (Tier 2). The completeness pass (Step 3.6) gaps you derive yourself by reading the repo use `"inferred"`. Both kinds coexist: a `completeness-gap` can be grounded (the engine proved it) or inferred (you reasoned it).
- `confidence`: `"confirmed"` or `"uncertain"` (default `"confirmed"`). Set by the Step 3.7 adversarial-verification pass for inferred findings: `"uncertain"` when you could neither confirm nor refute the concern (surfaced but flagged). Refuted findings are dropped, not emitted. Grounded findings are always `"confirmed"`. Omit to accept the default.

`completeness-gap` findings come from the Step 3.6 deliberate completeness pass — they represent things the change implies must also exist but don't. Example: `type: "completeness-gap", severity: "high", title: "ClickHouse not provisioned", description: "Code migrates fraud alerts to ClickHouse (chapter 6) but no provisioning found in infrastructure/ — confirm infra was added before merge", locations: ["src/fraud/store.ts"]`

**Good:** `type: "security", severity: "high", title: "Unvalidated user input", description: "User-provided ID passed directly to database query — confirm input is validated and parameterized"`
**Bad:** `description: "Worth understanding"` (no action, vague)

### complexity

Object with:
- `level`: one of `low`, `medium`, `high`, `very-high`
- `reasoning`: brief explanation (e.g., "New DB schema plus multiple service changes")

### Style

Talk like a coworker, not a changelog. No jargon, no filler phrases, no "this change introduces/implements/adds". Just say what happened and why it matters.

## Step 5 — Write agent output

Compute a unique temp path and write the JSON via a bash heredoc:

```bash
AGENT_OUTPUT=$(mktemp "${TMPDIR:-/tmp}/readthru-agent-output.XXXXXX")
cat > "$AGENT_OUTPUT" << 'AGENT_EOF'
{
  "chapters": [ ... ],
  "prologue": { ... }
}
AGENT_EOF
```

The trailing `XXXXXX` (with no suffix after) is required by macOS BSD `mktemp`. Using `cat` with a heredoc avoids tool-specific file-writing issues.

Field rules:

| Field | Constraint |
|-------|------------|
| `chapters[].id` | Non-empty, unique within the run |
| `chapters[].order` | Positive integer (1-indexed) |
| `chapters[].hunkRefs[].oldStart` | Non-negative integer — the pre-image start line from the `oldStart` in the formatted hunk header (`0` for new files) |
| `chapters[].keyChanges[].lineRefs` | Array with at least one entry |
| `lineRefs[].side` | `"additions"` (right side) or `"deletions"` (left side) |
| `lineRefs[].startLine` / `endLine` | Positive integers; `endLine >= startLine` |
| `prologue` | Optional object; omit entirely if not desired |
| `prologue.motivation` | String or `null` |
| `prologue.outcome` | String or `null` |
| `prologue.diagram` | Mermaid source string (no code fences) or `null`; omit for most changes |
| `prologue.keyChanges` | Array of 2–5 objects with `summary` and `description` |
| `prologue.focusAreas` | Array of 1–5 objects |
| `prologue.focusAreas[].type` | One of: `security`, `breaking-change`, `high-complexity`, `data-integrity`, `new-pattern`, `architecture`, `performance`, `testing-gap`, `completeness-gap` |
| `prologue.focusAreas[].provenance` | `"grounded"` or `"inferred"` (default `"inferred"`); omit to accept the default |
| `prologue.focusAreas[].confidence` | `"confirmed"` or `"uncertain"` (default `"confirmed"`); set `"uncertain"` only via the Step 3.7 verification pass; omit to accept the default |
| `prologue.focusAreas[].severity` | One of: `critical`, `high`, `medium`, `info` |
| `prologue.complexity.level` | One of: `low`, `medium`, `high`, `very-high` |

## Step 5.5 — Machine validation loop

Before displaying anything, machine-validate hunk coverage. This runs in **both modes**.

Run `readthru show --check` against the written output, passing the **same refs/flags** used for `prep`:

```bash
readthru show --check "$AGENT_OUTPUT"
# or, with the same scope you used for prep:
readthru show --check "$AGENT_OUTPUT" main..feature
readthru show --check --base main --compare feature "$AGENT_OUTPUT"
```

- **Exit 0** (`ok — <N> chapters cover <M> hunks` on stdout): proceed to Step 6.
- **Exit 1** (validation error on stderr — missing, extra, or duplicate hunks): fix the listed coverage errors in `$AGENT_OUTPUT` and re-run. **Maximum 3 attempts.** If it still fails after the third attempt, stop and report the remaining errors to the user — do not launch the UI.

## Step 6 — Display generated chapters

Hand the file to `readthru`:

```bash
readthru show "$AGENT_OUTPUT"
```

`readthru show` auto-detects the agent output format, independently computes the scope and "Other changes" chapter for filtered files, validates the JSON, inserts the run into the local SQLite database, boots a loopback HTTP server, and opens the browser.

**The command blocks until the user presses Ctrl+C.** If your harness requires non-blocking execution, run it in the background (e.g., `run_in_background` in Claude Code). Invoke it as the final command in the workflow.

When the user re-runs `/readthru-chapters` on the same repo and scope, `readthru show` automatically carries over read-state (viewed chapters, key-change annotations) from the previous review for chapters whose content is unchanged — reviewers can pick up exactly where they left off. If the user explicitly asks for a completely fresh, holistic review with no carry-over, pass `--fresh` to `readthru show`.
