01 Install #
Homebrew (recommended)
The repo is its own tap — one command installs the rw binary:
Upgrade later with brew upgrade recap-wins. The formula builds from source, so the first install compiles via Swift and takes a minute. Then skip to §2 — rw is already on your PATH.
From source
Requires Swift 6 and a recent macOS.
The binary is at .build/release/rw. Put it on your PATH so you can run rw from any repo. Pick one:
Verify:
All three options point at .build/release/rw inside the repo. Rebuilding keeps them working; moving or deleting the repo breaks them. The Homebrew tap is a nicer experience for a personal tool.
Repo docs
The source repo ships three primary docs at its root: GUIDE.md (this page's terminal-friendly counterpart), CONTRIBUTING.md (build/test, conventional-commit expectation, the open-core scope line — single-repo work is welcome; cross-repo aggregation is the paid layer), and RELEASING.md (the two-PR release flow grounded in scripts/release.sh). The README's "Docs" section links all three.
02 The mental model #
Every rw command analyzes a change set: the diff between a head ref and a base ref, in a repo. Three independent knobs:
| Knob | Flag | Default | Meaning |
|---|---|---|---|
| Repo | --repo <path> | current directory | which git repo to read |
| Base | --base <ref> | main | what you compare against |
| Head | --head <ref> | HEAD | what you compare |
So rw many --base develop means: in the repo I'm in, count what my current branch added on top of develop.
--base and --head accept any git ref — a branch, tag, or SHA:
Offline and read-only
The deterministic commands never write to the repo, never fetch, and never call a model. origin/* refs reflect your last git fetch — rw won't fetch for you.
Classification
Counts come first from conventional-commit prefixes (feat:, fix:, chore:, …) — the authoritative signal, never overridden. For commits with no recognized prefix, rw infers a bucket from the subject verbs (add/ship/implement → feature; fix/resolve → fix; bump/refactor/rename → chore) and the diff (a change set that adds new source files leans feature).
Inferred counts are flagged in the output — e.g. (2 inferred from message/diff — not conventional commits) — so you can always tell a declared count from a guessed one. The parsed type is never overwritten; --json shows both type and inferredBucket. For the richest read on non-conventional history, rw new (semantic) reads the full diff and recovers features even more precisely.
03 Your first run #
You get the vitals dashboard: feature/fix/chore counts, files changed with insertions/deletions, contributors, branches involved, the highest-churn files (hotspots), and any advisory risk flags (large diff, core files touched, source changed with no tests alongside). Risk flags are a nudge, never a gate.
Everything here is deterministic and instant — no key, no network.
04 Commands #
| Command | What it does | Mode |
|---|---|---|
| rw (default) | vitals dashboard (plain text only) | offline |
| rw vitals | vitals dashboard with full --json / --html | offline |
| rw many | count features / fixes / chores + breakdown | offline |
| rw blame | attribution by contributor | offline |
| rw branch | which branches contributed | offline |
| rw new | list the new user-facing features | semantic |
| rw notes <target> | write a review/release note | semantic |
| rw market --product <id> | a marketing content pack in the product's voice | semantic |
| rw align --product <id> | cross-port parity — matcher, HTML matrix, drafted issues | semantic |
| rw help [topic] | the built-in guide | — |
Global flags on every subcommand: --repo, --base, --head, --json (emit the raw change_report.json instead of the formatted view — works offline on any command, including the semantic ones), and --html (render the same report to a single self-contained, offline HTML file — see §6). --html takes two companions: --html-out <path> to override the default location and --open to launch the file in your default browser after writing.
Bare rw (no subcommand) only prints the formatted vitals dashboard as plain text. For the JSON or HTML versions of vitals, use the explicit rw vitals subcommand — those flags don't live on the root command anymore (it would shadow the same-named flags on subcommands like align). Existing scripts that piped rw --json > report.json need rw vitals --json > report.json.
rw align — cross-port parity
rw align compares two native ports of the same product (Ledgerly iOS in Swift and Ledgerly Android in Kotlin, say) and surfaces parity gaps. Unlike every other rw command, it reads two repos with no shared git history — there's nothing for git diff to compare. Instead it builds a feature ledger for each side, runs a semantic matcher that classifies each pairing, drafts tracker-agnostic issues for confirmed gaps, and (optionally) renders the whole thing as a filterable HTML parity matrix.
Input modes
Output modes
--json on rw align emits an AlignReport, not the change_report.json you get from the other commands. The two views answer different questions.
How the matcher classifies
Each feature pairing gets one of five labels with a confidence score:
paired— same capability on both sides; real parity.equivalent— different but platform-native substitutes (Apple Pay ↔ Google Pay). Parity achieved, not a gap.gap_on_a/gap_on_b— present on one side only; real missing work, with a drafted issue ready to paste.ambiguous— the matcher isn't sure; surfaced for human confirmation, never auto-classified as a gap.
A built-in Apple↔Google equivalence table ships with rw — Apple Pay/Google Pay, StoreKit/Play Billing, Keychain/Keystore, HealthKit/Health Connect, APNs/FCM, iCloud/Drive, WidgetKit/Glance, SwiftUI/Compose, Core Data/Room, Combine/Flow, TestFlight/Play Internal Testing, Sign in with Apple/Google Sign-In. Per-product [[product.parity.equivalent]] entries extend it.
The parity matrix (--html)
--html renders the report as a single self-contained, offline HTML file — same aesthetic as the rest of rw's HTML output. Two-column layout (side A left, side B right), each feature carrying its status chip and confidence pill. Filter chips at the top let you focus on just gaps, just ambiguous, etc. Drafted issues appear below the matrix with copy buttons. --html-out <path> overrides the default .rw/align-<a>-<b>.html; --open launches it in the browser after writing.
The pre-v0.3 names --matrix, --matrix-out, --open-matrix, --page, and --emit-json still work as aliases for one release. Prefer --html and --json going forward; the aliases go away in v0.4.
Drafted issues (--issues)
The matcher's IssueDraft body is canonical Markdown. --issues adds tracker-flavored wrappers without any API call: markdown (default), linear (h1 title + Labels: line), github (Title: / Labels: prefixes that pipe cleanly into gh issue create), jira (wiki markup — h2., *bold*, {{code}}, * bullets).
Confirming equivalences (--confirm)
When the matcher flags an equivalent or ambiguous pair you agree with, rw align --product <id> --confirm <match-id> appends a [[product.parity.equivalent]] entry to testthese.toml so it stops re-flagging on every run. Idempotent: re-confirming the same pair is a no-op.
Configuration
Every AlignReport ships with a non-optional disclaimer: parity is presented as candidates to confirm, not authoritative parity. A parity tool that's confidently wrong makes you relax when you shouldn't.
05 Writing prose: new and notes #
These two turn the change set into text. They need a provider (see §7) — either an API key, or skill mode with no key.
rw new
Lists the genuinely new, user-facing features, folding out chores and refactors.
rw notes <target>
Writes a note for exactly one target. Each has its own audience, tone, and character ceiling:
| Flag | Destination | Voice? | Cap |
|---|---|---|---|
| --pr | PR description (technical) | no | none |
| --asc-reviewer | App Store Connect → App Review (private) | no | none |
| --what-new | TestFlight / Play testing notes | yes | platform |
| --asc-update | App Store "What's New" | yes | 4000 |
| --gp-update | Google Play release notes | yes | 500/lang |
| --changelog | Release-page changelog (always HTML) | no | none |
rw notes --changelog
A dedicated target for the release-page changelog you commit per version. Always renders HTML (no terminal form) and defaults its output to docs/changelogs/v<version>.html. Generates a clean Added / Changed / Fixed page with a "Generated by rw" credit — the canonical "what changed" artifact for a release, not a paste-into-form note.
Product voice
The user-facing store targets read voice + a soft length aim from the product profile, so pass --product <id> (configure profiles in testthese.toml, §9).
Character caps are hard ceilings
rw warns if output would overflow — it never silently truncates. --limit <n> only tightens; it can't raise the ceiling past the store limit. Ceilings come from a TTL-cached limits.json manifest with a baked-in offline fallback; --refresh-limits forces a refetch.
Platform
--what-new keys off the product's platform: generous on iOS (TestFlight), tight on Android (Play, 500/lang).
06 HTML output #
Every command takes --html, which renders the same report to a single self-contained, offline HTML file — inline CSS/JS, no CDN, no server, no extra model calls beyond what the command already made. Use it when terminal scrollback is the wrong surface for what you produced: a marketing pack you'd rather eyeball before pasting, a PR description with a live cap meter, or a changelog you commit to the repo.
The default output path is .rw/<command>-<range>.html under the repo. Override with --html-out <path>, or pass --open to launch the file in your default browser after writing.
What renders, per command
rw/rw many/rw blame/rw branch— the deterministic dashboards as a static page (counts, hotspots, attribution, risk flags).rw new— the feature list as a clean reading view.rw notes— the note rendered in context with a live char meter against its store cap: plain at first, amber as you near the limit, red when you exceed it.rw market— an App Store / Play Console proof sheet: each piece (What's New, promo text, subtitle, post, tweet) in its own block with a live cap meter and a copy button. The page you'd want before pasting into a store form.rw notes --changelog— a release-page changelog (always HTML; see §5).
The .rw/ directory
The default output folder is added to .gitignore, so generated HTML doesn't end up tracked. The one exception is rw notes --changelog, which writes under docs/changelogs/ by design — that one is meant to be committed.
--html doesn't apply when --provider skill is in play — in skill mode the host agent is the renderer, not rw. Combining the two raises a clean validation error rather than silently doing nothing.
07 Providers — who writes the prose #
The semantic commands run through a provider, chosen by the provider key in testthese.toml or the --provider flag (the flag wins):
| Provider | Needs a key? | What it does |
|---|---|---|
| anthropic (default) | yes | Calls the Anthropic API and prints the prose |
| gemini | yes | Calls the Google Gemini API and prints the prose |
| skill | no | Emits JSON for a host agent to complete (see §8) |
API key (anthropic / gemini): set the provider's env var — ANTHROPIC_API_KEY or GEMINI_API_KEY (preferred) — or api_key in testthese.toml. The environment variable wins. With gemini selected and the model left at the Anthropic default, rw automatically uses a Gemini default model. If no key is set, the semantic commands exit with a clear message — not a crash.
A Claude Max/Pro plan covers claude.ai and Claude Code, not the pay-as-you-go Anthropic API. If you don't have API credits, use skill mode (§8) — it routes the model work through your agent session at no extra cost.
08 Skill mode — no API key #
In skill mode, rw does all the deterministic work and emits a JSON envelope; a host agent (Claude Code, Cowork) reads it and writes the prose. No key, no network call from rw — the agent you already pay for is the model.
From the CLI
As an installed skill (recommended)
rw ships a drop-in skill in the skill/ directory. Install it once:
Then, in a skill-aware agent, just ask in plain language:
- "What did I ship on this branch?"
- "Write a PR description for this branch."
- "Write the App Store release notes for this update."
The agent runs rw, reads the change report, and writes the answer — grounded in your real commits and diff, within any character caps.
Skills load at session start. After installing, start a fresh agent session so it's picked up.
The envelope
The JSON new/notes emit in skill mode contains:
system— the prompt (tone, audience, structure) the agent followsuser— the grounded change set; facts to write from, never to inventsoftTargetChars/ceilingChars— the length aim and the hard limit (0= uncapped)report— the full deterministicchange_report.json
09 Configuration #
Per-repo, human-readable. Drop a testthese.toml in the repo root. Every key is optional — rw works with no config. Copy testthese.toml.example to start.
Two tiers of limit, kept distinct:
- Ceilings (
[review_notes.limits]) — hard platform caps. Never exceeded; rw warns on overflow. - Targets (per-product) — soft editorial aims the model shoots for, so a 4000-char ceiling doesn't yield a 4000-char wall.
The real testthese.toml is gitignored so an api_key can't be committed. The tracked file is testthese.toml.example.
10 Recipes #
See what a branch introduced, offline
Write a PR description without leaving the terminal (with an API key)
Same, but key-free via your agent
App Store release notes in a product's voice, within the cap
Recap a repo you're not in
Feed the structured report to another tool
Preview a marketing pack before pasting into the store
Commit a release-page changelog
11 Troubleshooting #
which rw.--repo doesn't point at) a git work tree.rw new (semantic), which reads the diff and recovers the real features.ANTHROPIC_API_KEY or GEMINI_API_KEY), or use --provider skill for the key-free path (§8).--limit <n>, or regenerate; the store form is the final authority.git fetch first.