diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
new file mode 100644
index 0000000..23d0224
--- /dev/null
+++ b/skills/stage-chapters/SKILL.md
@@ -0,0 +1,159 @@
+---
+name: stage-chapters
+description: Generate Stage chapters for the current local git branch and open them in a browser for review.
+user-invocable: true
+---
+
+# stage-chapters
+
+Generates a Stage chapter run for the current local git branch and opens it in a browser. The skill detects the base ref, computes the diff, generates chapters from it, writes a JSON file matching the `stage-cli` schema, and hands the file to `stage-cli show` to launch the SPA.
+
+## Prerequisites
+
+Run these checks before any other work. If either fails, stop with the error message — do not continue.
+
+1. **`stage-cli` is installed.** Run `which stage-cli`. If it exits non-zero, instruct the user:
+
+ ```
+ stage-cli is not installed. Run:
+
+ npm install -g stagereview
+
+ Then retry /stage-chapters.
+ ```
+
+ Stop.
+
+2. **The current directory is a git repo.** Run `git rev-parse --is-inside-work-tree`. If it does not print `true`, stop with:
+
+ ```
+ /stage-chapters must be run inside a git repository.
+ ```
+
+## Step 1 — Detect base ref
+
+Find the branch the user reviews against. Try each of the following in order; the first that succeeds becomes ``:
+
+1. `git rev-parse --abbrev-ref origin/HEAD 2>/dev/null` — typically prints `origin/main`. Use the full output (e.g. `origin/main`) as ``; do **not** strip `origin/`, because the bare name (`main`) may not exist locally in single-branch clones.
+2. `git rev-parse --verify main 2>/dev/null` — local `main` branch; use `main` as ``.
+3. `git rev-parse --verify master 2>/dev/null` — older repos; use `master` as ``.
+4. `git rev-parse --verify origin/main 2>/dev/null` — remote-tracking fallback when `origin/HEAD` is unset; use `origin/main` as ``.
+5. `git rev-parse --verify origin/master 2>/dev/null` — remote-tracking fallback for older repos; use `origin/master` as ``.
+
+If all five fail, stop with:
+
+```
+No default branch detected. Tried origin/HEAD, main, master, origin/main, and origin/master.
+```
+
+`` is whatever ref expression was verified above and is passed verbatim to `git merge-base` / `git rev-parse` in Step 2.
+
+## Step 2 — Get the diff
+
+Compute the merge-base and dump the unified diff for the committed range only:
+
+```bash
+MERGE_BASE=$(git merge-base HEAD)
+HEAD_SHA=$(git rev-parse HEAD)
+
+git diff "$MERGE_BASE..HEAD"
+```
+
+If `git merge-base` exits non-zero or `MERGE_BASE` is empty, stop with an error like `Could not compute merge-base between and HEAD (unrelated histories or shallow clone).` Do **not** continue — running `git diff "..HEAD"` with an empty `MERGE_BASE` produces an empty diff that would silently yield zero chapters.
+
+`git diff ..HEAD` covers exactly the commits on the branch since it diverged from the base — the same range the SPA renders for a `committed` run (`baseSha..headSha` in `packages/cli/src/routes/diff.ts`). Save the full diff text into context for Step 3.
+
+This skill scopes review to *committed* work. If the user has uncommitted changes to tracked files, instruct them to commit first; mixing committed and working-tree changes into a single run would produce `hunkRefs`/`lineRefs` that don't line up with the diff the SPA serves.
+
+`MERGE_BASE` and `HEAD_SHA` are full 40-character SHAs that feed directly into the JSON `scope` field in Step 4.
+
+## Step 3 — Cluster + narrate
+
+> **TODO:** See Issue 11 for the chapter generation prompt port. For now, leave this step as a placeholder. The next revision of this skill will produce a `chapters` array shaped per the schema documented in Step 4.
+
+## Step 4 — Write JSON file
+
+Compute a unique temp path. The trailing `XXXXXX` (with no suffix after) is required by macOS BSD `mktemp` — placing characters after the X's causes BSD `mktemp` to return the template verbatim instead of substituting random characters:
+
+```bash
+TMPFILE=$(mktemp "${TMPDIR:-/tmp}/stage-chapters.XXXXXX")
+```
+
+`stage-cli show` reads JSON regardless of file extension, so the missing `.json` suffix is fine.
+
+The `${TMPDIR:-/tmp}` fallback matters on macOS, where `os.tmpdir()` resolves to `/var/folders/...` but `$TMPDIR` is not always set in every shell. Avoid `date +%s%N` — the `%N` (nanoseconds) format is a GNU extension and on macOS BSD `date` it emits a literal `N`, breaking uniqueness.
+
+Write a JSON file at `"$TMPFILE"` matching the shape below. The file must validate against `ChaptersFileSchema` in `packages/cli/src/schema.ts`; mismatched fields will be rejected by `stage-cli show`.
+
+High-level shape:
+
+```
+{ scope: {...}, chapters: [...], generatedAt: "..." }
+```
+
+Full example:
+
+```jsonc
+{
+ "scope": {
+ "kind": "committed",
+ // Set baseSha = mergeBaseSha = $MERGE_BASE so the SPA renders
+ // baseSha..headSha — the same range chapters were generated from.
+ "baseSha": "",
+ "headSha": "",
+ "mergeBaseSha": ""
+ },
+ "chapters": [
+ {
+ "id": "ch-1",
+ "order": 1,
+ "title": "Short imperative title",
+ "summary": "Why this chapter matters to the reviewer.",
+ "hunkRefs": [
+ { "filePath": "path/to/file.ts", "oldStart": 42 }
+ ],
+ "keyChanges": [
+ {
+ "content": "A judgment-call question for the reviewer (not source code).",
+ "lineRefs": [
+ {
+ "filePath": "path/to/file.ts",
+ "side": "additions",
+ "startLine": 50,
+ "endLine": 55
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "generatedAt": "2026-05-04T12:34:56.000Z"
+}
+```
+
+Field rules:
+
+| Field | Constraint |
+|-------|------------|
+| `scope.kind` | `"committed"` or `"workingTree"` |
+| `scope.ref` | Required when `kind` is `"workingTree"`; one of `"work"`, `"staged"`, `"unstaged"` |
+| `scope.baseSha` / `headSha` / `mergeBaseSha` | Full 40-character lowercase hex SHAs |
+| `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 unified-diff `@@` 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` |
+| `generatedAt` | ISO 8601 datetime string |
+
+## Step 5 — Display generated chapters
+
+Hand the file to `stage-cli`:
+
+```bash
+stage-cli show "$TMPFILE"
+```
+
+`stage-cli show` validates the JSON, inserts a new `chapter_run` plus chapters and key changes into the local SQLite database, boots a loopback HTTP server, and opens the browser to the new run. The command stays running and serves the SPA until the user kills it with Ctrl+C — invoke it as the final command in the workflow rather than expecting it to print a value and exit.
+
+Do not pass a `runId` and do not call a separate `stage-cli ingest`. `show ` does ingestion and serving in one step.