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.