From bc2170db14c30ee13019cb0bd8373b2db263bc53 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 00:49:50 -0700
Subject: [PATCH 1/6] feat(AI-81): author stage-chapters SKILL.md scaffold
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds skills/stage-chapters/SKILL.md at the repo root with vercel-labs/skills
frontmatter, prerequisites (stage-cli + git repo checks), base ref detection
(origin/HEAD → main → master), merge-base diff retrieval, JSON-shape
documentation matching ChaptersFileSchema, and the final stage-cli show step.
Step 3 (cluster + narrate) is left as a TODO pointing at Issue 11.
---
skills/stage-chapters/SKILL.md | 158 +++++++++++++++++++++++++++++++++
1 file changed, 158 insertions(+)
create mode 100644 skills/stage-chapters/SKILL.md
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
new file mode 100644
index 0000000..f1769f9
--- /dev/null
+++ b/skills/stage-chapters/SKILL.md
@@ -0,0 +1,158 @@
+---
+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 stage-cli
+
+ 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; use the first that succeeds:
+
+1. `git rev-parse --verify origin/HEAD 2>/dev/null` — typically resolves to `refs/remotes/origin/main`. Strip the leading `refs/remotes/origin/` to recover the bare base name.
+2. `git rev-parse --verify main` — fall back to a local `main` branch.
+3. `git rev-parse --verify master` — final fallback for older repos.
+
+If all three fail, stop with:
+
+```
+No default branch detected. Tried origin/HEAD, main, and master.
+```
+
+Save the resolved base name (e.g. `main`) as `` for the next steps.
+
+## Step 2 — Get the diff
+
+Compute the merge-base, capture the relevant SHAs, and dump the unified diff:
+
+```bash
+MERGE_BASE=$(git merge-base HEAD)
+BASE_SHA=$(git rev-parse )
+HEAD_SHA=$(git rev-parse HEAD)
+
+git diff "$MERGE_BASE"..
+```
+
+The trailing `..` is intentional — it includes working-tree changes for tracked files alongside committed changes on the branch. Save the full diff text into context for Step 3.
+
+`BASE_SHA`, `HEAD_SHA`, and `MERGE_BASE` are full 40-character SHAs and 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. Epoch nanoseconds keep concurrent runs from colliding:
+
+```bash
+TMPFILE="${TMPDIR:-/tmp}/stage-chapters-$(date +%s%N).json"
+```
+
+The `${TMPDIR:-/tmp}` fallback matters on macOS, where `os.tmpdir()` resolves to `/var/folders/...` but `$TMPDIR` is not always set in every shell.
+
+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": {
+ // Either committed (no working-tree changes):
+ "kind": "committed",
+ "baseSha": "<40-char SHA>",
+ "headSha": "<40-char SHA>",
+ "mergeBaseSha": "<40-char SHA>"
+
+ // Or workingTree (when the diff includes uncommitted tracked changes):
+ // "kind": "workingTree",
+ // "ref": "work" | "staged" | "unstaged",
+ // "baseSha": "<40-char SHA>",
+ // "headSha": "<40-char SHA>",
+ // "mergeBaseSha": "<40-char SHA>"
+ },
+ "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.
From a14ab7b59b0baf9adbaff1eeb48676d17f131afb Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 01:04:57 -0700
Subject: [PATCH 2/6] fix(AI-81): correct stage-chapters SKILL.md per PR review
- Use the published npm package name `stagereview` (binary is `stage-cli`)
in the install instructions
- Use `git rev-parse --abbrev-ref origin/HEAD` for base detection so the
output is `origin/main` (a strippable ref name), not a commit SHA
- Drop the trailing `..` from `git diff "$MERGE_BASE"` so the output
actually includes uncommitted working-tree changes for tracked files
- Replace `date +%s%N` with `mktemp` template so unique filename
generation works on macOS BSD `date` (which has no `%N`)
---
skills/stage-chapters/SKILL.md | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
index f1769f9..ec0a050 100644
--- a/skills/stage-chapters/SKILL.md
+++ b/skills/stage-chapters/SKILL.md
@@ -17,7 +17,7 @@ Run these checks before any other work. If either fails, stop with the error mes
```
stage-cli is not installed. Run:
- npm install -g stage-cli
+ npm install -g stagereview
Then retry /stage-chapters.
```
@@ -34,9 +34,9 @@ Run these checks before any other work. If either fails, stop with the error mes
Find the branch the user reviews against. Try each of the following in order; use the first that succeeds:
-1. `git rev-parse --verify origin/HEAD 2>/dev/null` — typically resolves to `refs/remotes/origin/main`. Strip the leading `refs/remotes/origin/` to recover the bare base name.
-2. `git rev-parse --verify main` — fall back to a local `main` branch.
-3. `git rev-parse --verify master` — final fallback for older repos.
+1. `git rev-parse --abbrev-ref origin/HEAD 2>/dev/null` — typically resolves to `origin/main` (exits non-zero when `origin/HEAD` is unset). Strip the leading `origin/` to recover the bare base name.
+2. `git rev-parse --verify main 2>/dev/null` — fall back to a local `main` branch.
+3. `git rev-parse --verify master 2>/dev/null` — final fallback for older repos.
If all three fail, stop with:
@@ -55,10 +55,10 @@ MERGE_BASE=$(git merge-base HEAD)
BASE_SHA=$(git rev-parse )
HEAD_SHA=$(git rev-parse HEAD)
-git diff "$MERGE_BASE"..
+git diff "$MERGE_BASE"
```
-The trailing `..` is intentional — it includes working-tree changes for tracked files alongside committed changes on the branch. Save the full diff text into context for Step 3.
+`git diff ` (no `..`) compares `` to the working tree, so the output covers committed changes on the branch *plus* uncommitted edits to tracked files. Save the full diff text into context for Step 3.
`BASE_SHA`, `HEAD_SHA`, and `MERGE_BASE` are full 40-character SHAs and feed directly into the JSON `scope` field in Step 4.
@@ -68,13 +68,13 @@ The trailing `..` is intentional — it includes working-tree changes for tracke
## Step 4 — Write JSON file
-Compute a unique temp path. Epoch nanoseconds keep concurrent runs from colliding:
+Compute a unique temp path. `mktemp` with an `XXXXXX` template is portable across macOS and Linux and avoids collisions even within the same second:
```bash
-TMPFILE="${TMPDIR:-/tmp}/stage-chapters-$(date +%s%N).json"
+TMPFILE=$(mktemp "${TMPDIR:-/tmp}/stage-chapters-XXXXXX.json")
```
-The `${TMPDIR:-/tmp}` fallback matters on macOS, where `os.tmpdir()` resolves to `/var/folders/...` but `$TMPDIR` is not always set in every shell.
+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`.
From 6653b18178b2c4e9d89eea5b75b566ea88f1aa76 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 01:12:09 -0700
Subject: [PATCH 3/6] fix(AI-81): keep full origin/HEAD ref so base works in
single-branch clones
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Previously the skill stripped `origin/` from `origin/main`, leaving ``
set to `main`. In single-branch clones (and after deleting a local `main`)
that local ref does not exist, so the subsequent `git merge-base HEAD`
and `git rev-parse ` calls fail. Use the full remote-tracking ref
(`origin/main`) as `` directly — git accepts it everywhere a commit-ish
is expected.
---
skills/stage-chapters/SKILL.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
index ec0a050..db5fc10 100644
--- a/skills/stage-chapters/SKILL.md
+++ b/skills/stage-chapters/SKILL.md
@@ -32,11 +32,11 @@ Run these checks before any other work. If either fails, stop with the error mes
## Step 1 — Detect base ref
-Find the branch the user reviews against. Try each of the following in order; use the first that succeeds:
+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 resolves to `origin/main` (exits non-zero when `origin/HEAD` is unset). Strip the leading `origin/` to recover the bare base name.
-2. `git rev-parse --verify main 2>/dev/null` — fall back to a local `main` branch.
-3. `git rev-parse --verify master 2>/dev/null` — final fallback for older repos.
+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 ``.
If all three fail, stop with:
@@ -44,7 +44,7 @@ If all three fail, stop with:
No default branch detected. Tried origin/HEAD, main, and master.
```
-Save the resolved base name (e.g. `main`) as `` for the next steps.
+`` is whatever ref expression was verified above (`origin/main`, `main`, or `master`) and is passed verbatim to `git merge-base` / `git rev-parse` in Step 2.
## Step 2 — Get the diff
From 01f1cc910634b25693ae766a9071f0d387cbd166 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 01:26:18 -0700
Subject: [PATCH 4/6] fix(AI-81): align chapter diff with SPA rendering, fix
BSD mktemp template
- Step 2 now uses `git diff "$MERGE_BASE..HEAD"` (committed range only)
so chapter `hunkRefs`/`lineRefs` reference the exact range the SPA
renders for `committed` scope (`baseSha..headSha` per
`packages/cli/src/routes/diff.ts`). Mixing in working-tree changes
would produce hunks the SPA cannot show; instruct users to commit
uncommitted edits first.
- JSON example now sets `baseSha = mergeBaseSha = $MERGE_BASE` so the
SPA's `baseSha..headSha` query matches the chapter-generation range
even when the base branch has advanced past the merge-base.
- mktemp template is `stage-chapters.XXXXXX` (no `.json` suffix). BSD
`mktemp` returns the template verbatim when characters follow the
X's, breaking uniqueness on macOS. `stage-cli show` reads JSON
regardless of extension.
---
skills/stage-chapters/SKILL.md | 33 +++++++++++++++------------------
1 file changed, 15 insertions(+), 18 deletions(-)
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
index db5fc10..ef4fb72 100644
--- a/skills/stage-chapters/SKILL.md
+++ b/skills/stage-chapters/SKILL.md
@@ -48,19 +48,20 @@ No default branch detected. Tried origin/HEAD, main, and master.
## Step 2 — Get the diff
-Compute the merge-base, capture the relevant SHAs, and dump the unified diff:
+Compute the merge-base and dump the unified diff for the committed range only:
```bash
MERGE_BASE=$(git merge-base HEAD)
-BASE_SHA=$(git rev-parse )
HEAD_SHA=$(git rev-parse HEAD)
-git diff "$MERGE_BASE"
+git diff "$MERGE_BASE..HEAD"
```
-`git diff ` (no `..`) compares `` to the working tree, so the output covers committed changes on the branch *plus* uncommitted edits to tracked files. Save the full diff text into context for Step 3.
+`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.
-`BASE_SHA`, `HEAD_SHA`, and `MERGE_BASE` are full 40-character SHAs and feed directly into the JSON `scope` field in Step 4.
+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
@@ -68,12 +69,14 @@ git diff "$MERGE_BASE"
## Step 4 — Write JSON file
-Compute a unique temp path. `mktemp` with an `XXXXXX` template is portable across macOS and Linux and avoids collisions even within the same second:
+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.json")
+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`.
@@ -89,18 +92,12 @@ Full example:
```jsonc
{
"scope": {
- // Either committed (no working-tree changes):
"kind": "committed",
- "baseSha": "<40-char SHA>",
- "headSha": "<40-char SHA>",
- "mergeBaseSha": "<40-char SHA>"
-
- // Or workingTree (when the diff includes uncommitted tracked changes):
- // "kind": "workingTree",
- // "ref": "work" | "staged" | "unstaged",
- // "baseSha": "<40-char SHA>",
- // "headSha": "<40-char SHA>",
- // "mergeBaseSha": "<40-char SHA>"
+ // Set baseSha = mergeBaseSha = $MERGE_BASE so the SPA renders
+ // baseSha..headSha — the same range chapters were generated from.
+ "baseSha": "",
+ "headSha": "",
+ "mergeBaseSha": ""
},
"chapters": [
{
From 11b78337021bf9461a8ac8396873223659258879 Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 01:34:07 -0700
Subject: [PATCH 5/6] fix(AI-81): add origin/main and origin/master to
base-detection fallbacks
When `origin/HEAD` is unset (some clones, or after the user deletes the
local default branch) and there is no local `main`/`master`, the skill
previously aborted even though `origin/main`/`origin/master` were valid
review bases. Append them as the last two fallbacks before giving up.
---
skills/stage-chapters/SKILL.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
index ef4fb72..75d20aa 100644
--- a/skills/stage-chapters/SKILL.md
+++ b/skills/stage-chapters/SKILL.md
@@ -37,14 +37,16 @@ Find the branch the user reviews against. Try each of the following in order; th
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 three fail, stop with:
+If all five fail, stop with:
```
-No default branch detected. Tried origin/HEAD, main, and master.
+No default branch detected. Tried origin/HEAD, main, master, origin/main, and origin/master.
```
-`` is whatever ref expression was verified above (`origin/main`, `main`, or `master`) and is passed verbatim to `git merge-base` / `git rev-parse` in Step 2.
+`` 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
From 1d2ff0f803dd39ba897dde97f13b9d47becebe5f Mon Sep 17 00:00:00 2001
From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com>
Date: Mon, 4 May 2026 01:43:46 -0700
Subject: [PATCH 6/6] fix(AI-81): fail fast when merge-base cannot be computed
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Without an explicit guard, `git merge-base HEAD` failing (unrelated
histories or shallow clones) leaves `MERGE_BASE` empty, and the subsequent
`git diff "..HEAD"` becomes `HEAD..HEAD` — an empty diff that would
silently produce zero chapters. Tell the agent to abort with a clear
error before continuing.
---
skills/stage-chapters/SKILL.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/skills/stage-chapters/SKILL.md b/skills/stage-chapters/SKILL.md
index 75d20aa..23d0224 100644
--- a/skills/stage-chapters/SKILL.md
+++ b/skills/stage-chapters/SKILL.md
@@ -59,6 +59,8 @@ 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.