Generate a static, interactive PR walkthrough from a GitHub pull request, git branch comparison, commit range, or raw diff β as a self-contained folder of HTML/CSS/JS you can zip, email, attach to a ticket, or open locally in a browser with no server.
Instead of dumping a flat file list, PatchStory tells the change as a story: logical chapters, each with intent, the relevant diff hunks, reviewer questions, risk level, and verification steps.
patchstory diff main...my-branch --out ./walkthrough
patchstory diff main...my-branch --zip
patchstory commits abc123..def456
patchstory file ./my-pr.diff
patchstory github https://github.com/org/repo/pull/123 -g anthropic
patchstory render ./pr-walkthrough.json --out ./site- Local-first. No hosted service, no database, no auth.
- Zero runtime dependencies. Custom diff parser, custom ZIP writer, built-in
fetchfor the optional AI adapter. Build-time deps only:esbuild,typescript, andhighlight.js(bundled intoapp.js, so the CLI itself ships nonode_modules). - Portable output. Opens over
file://(data embedded indata.js, nofetchrequired). Zip-friendly. - AI optional. A heuristic generator always works with no API key. AI improves the story; it is never required.
- Narrated play mode. Press βΆ Play to turn the walkthrough into a
self-playing screencast: each chapter becomes a scene that pans the actual
diff and spotlights the lines it references, narrated aloud via the browser's
built-in speech synthesis (captions included). No ffmpeg, no API key, no
network β the same single
.html, just playing itself.
A 6-file PR β "Add Redis-backed rate limiting to the public API" β turned into a narrated walkthrough by PatchStory. Watch it as a ~2.5-minute video (with audio) β a title card, then one animated scene per chapter where the diff reveals and the referenced lines light up as they're narrated:
demo.mp4
The source diff and the agent-authored story are tracked
in examples/rate-limiting.diff and
examples/rate-limiting.json; render the interactive
page below yourself with:
patchstory render examples/rate-limiting.json --diff examples/rate-limiting.diff --single-file --openThe overview maps the whole change: a "start here" reading list, the stats, the recurring themes, and a risk-rated reviewer path.
Open a chapter and you get its intent, the reviewer questions to ask, the verification steps, and the syntax-highlighted diff (unified or split, collapsible, with per-file "mark reviewed" state).
git clone https://github.com/russ/patchstory.git
cd patchstory
npm install # installs deps and builds (via the `prepare` script)
# try it on the bundled example (no git repo needed)
node packages/cli/dist/patchstory.mjs file ./examples/sample.diff --single-file --opennpm install runs the build automatically. To rebuild after changes, run
npm run build. You can npm link the patchstory package (or run
node packages/cli/dist/patchstory.mjs) to get the patchstory command on PATH.
Requirements: the built CLI runs on Node β₯ 20. Local development (the
npm test runner, which executes TypeScript directly) needs Node β₯ 22.6.
The guiding principle: the AI-generated story is separate from the renderer. The renderer consumes a JSON document + a parsed diff and does not care whether the story came from a heuristic, OpenAI, Claude, a local model, or a human.
βββββββββββββββ raw diff ββββββββββββββββ WalkthroughDocument ββββββββββββββββ
β sources β ββββββββββββΆ β generator β βββββββββββββββββββββΆ β renderer β
β git / gh / β β none | β + ParsedDiff β static site β
β file / PR β β anthropic β β (vanilla TS) β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ
\__________________________ pr-walkthrough.json ___________________________/
(canonical intermediate representation)
patchstory/
βββ package.json # workspace root; `build` + `typecheck` scripts
βββ build.mjs # esbuild: bundle web UI, inline assets, bundle CLI
βββ tsconfig*.json
βββ examples/
β βββ sample.diff # a multi-area diff for trying the tool
βββ packages/
βββ core/ # @patchstory/core β input + IR layer (zero deps)
β βββ src/
β βββ types.ts # WalkthroughDocument, ParsedDiff, β¦
β βββ diff.ts # unified-diff parser
β βββ git.ts # shells out to `git`
β βββ sources.ts # resolve diff/commits/file/github β raw diff
β βββ schema.ts # JSON Schema + runtime validator
β βββ generators/
β βββ types.ts # WalkthroughGenerator interface
β βββ none.ts # heuristic generator (no AI)
β βββ anthropic.ts # optional Claude adapter (raw fetch)
β βββ index.ts # getGenerator(name)
βββ renderer/ # @patchstory/renderer β IR β static site
β βββ web/ # the client UI source (vanilla TS + CSS)
β β βββ app.ts
β β βββ styles.css
β β βββ index.html
β βββ src/
β βββ index.ts # renderWalkthrough(bundle, outDir)
β βββ assets.generated.ts # built: inlined app.js/css/html
βββ cli/ # patchstory β the CLI entry point
βββ src/
βββ cli.ts # command dispatch
βββ args.ts # tiny argv parser
βββ zip.ts # pure-Node ZIP writer
The output must open over file:// with no server and be trivially zippable.
fetch() is blocked on file://, so the walkthrough data is embedded as
window.__PATCHSTORY__ in data.js. A single small bundled app.js (no
framework runtime) keeps the output lean and robust.
walkthrough/
βββ index.html # page shell
βββ data.js # window.__PATCHSTORY__ = { walkthrough, diff, docId }
βββ assets/
β βββ app.js # bundled UI
β βββ styles.css
βββ pr-walkthrough.json # canonical IR (also for other tools / AI agents)
patchstory <command> [args] [options]
Commands
diff <range> git range, e.g. main...feature
commits <range> commit range, e.g. abc123..def456
file <path.diff> raw unified diff file
github <pr-url> GitHub PR (uses `gh` if available, else public .diff)
render <walkthrough> render an existing pr-walkthrough.json
video <walkthrough> render a narrated .mp4 screencast of the walkthrough
serve [dir|file] serve an output folder/file on your LAN
schema print the pr-walkthrough.json JSON Schema
Options
-o, --out <path> output dir; .html file with --single-file;
.json file (or stdout) with --scaffold (default ./walkthrough)
-g, --generator <name> none | anthropic (default none)
--repo <dir> git repo to operate in (default cwd)
--model <id> model id for the anthropic generator
--scaffold emit the editable pr-walkthrough.json (the IR) instead
of rendering β for an agent or human to enrich
--emit-diff <file> with --scaffold: also write the resolved raw diff, so the
same bytes can be passed to `render --diff`
--single-file emit one self-contained .html (easy to email/attach)
--redact mask secrets in the diff before generating/rendering
--serve serve the result on your LAN after generating
--open open the result in a browser
--port <n> port for --serve / serve (default 8137)
--diff <file> (render/video) raw diff to fill the diff explorer
--zip also write <out>.zip
--tts <engine> (video) auto | elevenlabs | espeak-ng | flite | say | none
--voice <id> (video) voice id (elevenlabs) or name (espeak-ng/say)
--chrome <path> (video) Chrome/Chromium used to rasterize scenes
--fps <n> (video) frames per second (default 30)
--keep (video) keep the intermediate working dir
-h, --help show help
--version show version
The none generator never needs network or keys. With -g anthropic and
ANTHROPIC_API_KEY set, PatchStory asks Claude to author the chapters; on any
error it falls back to the heuristic walkthrough, so the command always
produces output.
Handing it off. Two smooth paths: --single-file produces one portable
.html you can email or attach to a ticket (everything inlined, opens over
file://); or --serve/serve hosts the output on your LAN and prints a URL
others can open. --redact masks secrets (token shapes, KEY=value, private
keys) in the diff before it's embedded or sent to an AI generator.
patchstory video <walkthrough.json> --diff <pr.diff> -o walkthrough.mp4 renders the
walkthrough into a real, shareable .mp4: a title card, one animated scene per
chapter (the diff reveals line-by-line and the referenced lines light up as they're
narrated), and an outro. Unlike everything else here, this shells out to system
tools β it adds no npm runtime deps, and they're only touched when you ask for a video.
Two engines (--engine):
hyperframes(default) β generates a HyperFrames composition (HTML + GSAP) and renders it frame-by-frame in headless Chrome vianpx hyperframes. This is the animated one. Needs network fornpxon first use.panβ a fully local fallback: rasterizes each scene with Chromium and pans it with ffmpeg. Nonpx/network; lower production value.
Text-to-speech (--tts, default auto): elevenlabs (ELEVENLABS_API_KEY, best
quality), kokoro (local neural TTS via HyperFrames β no key, the keyless default),
local espeak-ng / flite / macOS say, or none (silent; captions still shown).
ffmpeg/ffprobe are resolved from PATH, then /usr/bin, then
PATCHSTORY_FFMPEG / PATCHSTORY_FFPROBE β each validated by actually running it, so a
broken or shadowing PATH entry is skipped (and the working one is handed to HyperFrames).
Narration is the audio track β nothing is burned into or captioned over the frame, so the code and motion graphics stay unobstructed.
It's slower and heavier than the HTML β the in-page play mode is the local-first default; the MP4 is for when you need a file to drop in Slack or a release thread.
Syntax-highlighted diffs (highlight.js, bundled at build time β lazily applied as you scroll so big PRs stay fast), unified/split views, collapsible hunks with Expand all / Collapse all, sidebar chapter nav (slide-in drawer on mobile), search, file filters, per-reviewer "mark reviewed" state in localStorage, light/ dark, copy-summary, a "Start here" guide and recurring-theme detection on the overview, related commits per chapter, and a footer build stamp.
Keyboard: j/k next/prev chapter Β· / search Β· e/c expand/collapse all Β·
r toggle reviewed Β· p play narrated walkthrough Β· t theme Β· ? shortcuts Β·
Esc close. In play mode: space play/pause Β· β/β prev/next scene Β· m
mute (captions only) Β· Esc close.
PatchStory keeps the story (a JSON document) separate from the renderer, so you
don't need the built-in anthropic adapter to get an AI-written walkthrough β you can let
your own coding agent (Claude Code, Cursor, aider, β¦) author it. The agent reads the diff
in the context of the whole repo, so its narrative is usually better than a one-shot API
call, and no API key is involved.
The flow is three commands:
# 1. Scaffold a schema-valid skeleton from any source, plus the exact diff bytes.
patchstory github <pr-url> --scaffold -o pr-walkthrough.json --emit-diff pr.diff
# 2. Your agent rewrites pr-walkthrough.json into a real narrative β chapter intent,
# risk, reviewer questions, verification steps. Validate against the schema anytime:
patchstory schema > pr-walkthrough.schema.json
# 3. Render the agent's story. --redact keeps secrets out of the embedded diff.
patchstory render pr-walkthrough.json --diff pr.diff --redact --single-file -o pr.html --open--scaffold works with every source command (diff, commits, file, github) and runs
the none heuristic to hand the agent an accurate starting point β correct stats, file
groupings, and diff_hunks line numbers β which the agent then enriches. Because
--emit-diff writes the same bytes the scaffold was computed from, the hunk line refs stay
aligned when you render --diff them. (--redact masks the embedded diff at render time; the
scaffolded IR and emitted diff are left unredacted for the agent to read.)
A ready-to-install Claude Code plugin that runs exactly this flow lives in
integrations/claude-code/:
/plugin marketplace add russ/patchstory
/plugin install patchstory@russ-patchstory
It auto-detects the source, scaffolds, has Claude author the narrative, and renders. Other agents can follow the same three commands.
This is the canonical intermediate representation. The renderer consumes it; AI
agents (or humans) can generate or edit it directly, then run
patchstory render. The machine-readable JSON Schema lives in
packages/core/src/schema.ts and is validated at
load time. Shape:
Required: version, title, summary, source (+ source.type), stats,
chapters (each needs id, title, summary, risk_level, files).
Everything else is optional. start_line/end_line are line numbers in the
new file.
It groups changed files into thematic chapters and orders them into a sensible reading path:
- Data model & migrations (schema/migration files) β read first.
- Source modules, split by their leading directory (e.g.
app/models,app/services). - Styling, then Tests, then Build & configuration, then Docs.
Each chapter gets a heuristic risk_level (sensitive paths like auth/payment/
migrations, large changes, or whole-file deletions raise it), plus review
questions and verification steps tailored to the file type. It's genuinely
useful with no API key β AI just makes the narrative better.
Implement the one seam:
interface WalkthroughGenerator {
readonly name: string;
generate(input: DiffAnalysisInput): Promise<WalkthroughDocument>;
}Register it in packages/core/src/generators/index.ts. The renderer needs no
changes. openai and local (Ollama) are stubbed to fall back to none.
- Hosted SaaS, user accounts, database, real-time collaboration, plugin marketplace, GitHub App auth β explicitly out of scope.
openai/localgenerators β interface is in place; implementations are stubs that fall back tonone.- Private GitHub PRs without
ghβ the public.difffallback only covers public repos; authenticated fetch relies on theghCLI. - Inline per-line review comments / threads β only chapter-level notes today.
- Word-level intra-line diff highlighting β diffs are line-level (token-level syntax highlighting is done; the intra-line changed-region highlight is not).
- Per-commit file mapping for GitHub PRs β commitβchapter links work for
local
diff/commits;githubshows the commit list but can't map files to commits without extra API calls. - Rename/binary content rendering β detected and labeled, not deep-diffed.
- Very large diffs β everything embeds; highlighting is lazy but there's no pagination/virtualization of the DOM yet.
renderwithout a diff β synthesizes empty file entries so links resolve; pass--diff <raw.diff>to populate the diff explorer.
npm test # node --test over tests/*.ts β diff parser, none generator,
# schema validation, redaction, zip writerLicensed MIT (see LICENSE).
npm run build # bundle everything
npm run typecheck # tsc --noEmit across all packagesbuild.mjs bundles packages/renderer/web/app.ts to a browser IIFE, inlines it
(plus CSS/HTML) into assets.generated.ts, then bundles the CLI (pulling in core
- renderer) into a single self-contained
packages/cli/dist/patchstory.mjs.



{ "version": "0.1", "title": "Add multi-face media review workflow", "summary": "Introduces a review workflow for media with multiple detected faces.", "source": { "type": "github_pr", // github_pr | git_diff | commit_range | diff_file "repo": "org/repo", "pr_number": 123, "base": "main", "head": "feature/multi-face-review" }, "stats": { "files_changed": 12, "additions": 340, "deletions": 72 }, "themes": ["Data model & migrations", "Detection service", "Tests"], "reviewer_path": ["face-detection", "review-state", "tests"], "chapters": [ { "id": "face-detection", "title": "Detect multiple faces in uploaded media", "summary": "Adds metadata and detection logic for multi-face media.", "intent": "Determine whether creator approval is needed before publishing.", "risk_level": "medium", // low | medium | high "files": ["app/models/media.rb", "app/services/face_detection_service.rb"], "diff_hunks": [ { "file": "app/models/media.rb", "start_line": 42, "end_line": 88, "summary": "Adds face count and review state fields." } ], "review_notes": [ "Confirm single-face uploads are not accidentally blocked.", "Check what happens when face detection fails." ], "verification_steps": [ "Upload media with one face.", "Upload media with multiple faces.", "Upload media where detection returns no result." ] } ] }