diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 5311c2c..45768d0 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -200,6 +200,8 @@ Templates contain the workflows, tips, and examples that require human judgment.
| `{{SNAPSHOT_FLAGS}}` | `snapshot.ts` | Flag reference with examples |
| `{{PREAMBLE}}` | `gen-skill-docs.ts` | Startup block: update check, session tracking, contributor mode, AskUserQuestion format |
| `{{BROWSE_SETUP}}` | `gen-skill-docs.ts` | Binary discovery + setup instructions |
+| `{{BASE_BRANCH_DETECT}}` | `gen-skill-docs.ts` | Dynamic base branch detection for PR-targeting skills (ship, review, qa, plan-ceo-review) |
+| `{{QA_METHODOLOGY}}` | `gen-skill-docs.ts` | Shared QA methodology block for /qa and /qa-only |
This is structurally sound — if a command exists in code, it appears in docs. If it doesn't exist, it can't appear.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57c2c1a..3d67a91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.4.2 — 2026-03-16
+
+- **Skills now respect your branch target.** `/ship`, `/review`, `/qa`, and `/plan-ceo-review` detect which branch your PR actually targets instead of assuming `main`. Stacked branches, Conductor workspaces targeting feature branches, and repos using `master` all just work now.
+- **`/retro` works on any default branch.** Repos using `master`, `develop`, or other default branch names are detected automatically — no more empty retros because the branch name was wrong.
+- **New `{{BASE_BRANCH_DETECT}}` placeholder** for skill authors — drop it into any template and get 3-step branch detection (PR base → repo default → fallback) for free.
+- **3 new E2E smoke tests** validate base branch detection works end-to-end across ship, review, and retro skills.
+
+### For contributors
+
+- Added "Writing SKILL templates" section to CLAUDE.md — rules for natural language over bash-isms, dynamic branch detection, self-contained code blocks.
+- Hardcoded-main regression test scans all `.tmpl` files for git commands with hardcoded `main`.
+- QA template cleaned up: removed `REPORT_DIR` shell variable, simplified port detection to prose.
+- gstack-upgrade template: explicit cross-step prose for variable references between bash blocks.
+
## 0.4.1 — 2026-03-16
- **gstack now notices when it screws up.** Turn on contributor mode (`gstack-config set gstack_contributor true`) and gstack automatically writes up what went wrong — what you were doing, what broke, repro steps. Next time something annoys you, the bug report is already written. Fork gstack and fix it yourself.
diff --git a/CLAUDE.md b/CLAUDE.md
index e724b82..bc21f60 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -65,6 +65,23 @@ SKILL.md files are **generated** from `.tmpl` templates. To update docs:
To add a new browse command: add it to `browse/src/commands.ts` and rebuild.
To add a snapshot flag: add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts` and rebuild.
+## Writing SKILL templates
+
+SKILL.md.tmpl files are **prompt templates read by Claude**, not bash scripts.
+Each bash code block runs in a separate shell — variables do not persist between blocks.
+
+Rules:
+- **Use natural language for logic and state.** Don't use shell variables to pass
+ state between code blocks. Instead, tell Claude what to remember and reference
+ it in prose (e.g., "the base branch detected in Step 0").
+- **Don't hardcode branch names.** Detect `main`/`master`/etc dynamically via
+ `gh pr view` or `gh repo view`. Use `{{BASE_BRANCH_DETECT}}` for PR-targeting
+ skills. Use "the base branch" in prose, `` in code block placeholders.
+- **Keep bash blocks self-contained.** Each code block should work independently.
+ If a block needs context from a previous step, restate it in the prose above.
+- **Express conditionals as English.** Instead of nested `if/elif/else` in bash,
+ write numbered decision steps: "1. If X, do Y. 2. Otherwise, do Z."
+
## Browser interaction
When you need to interact with a browser (QA, dogfooding, cookie setup), use the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index abe1cf1..b06b837 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -217,6 +217,8 @@ bun run skill:check
bun run dev:skill
```
+For template authoring best practices (natural language over bash-isms, dynamic branch detection, `{{BASE_BRANCH_DETECT}}` usage), see CLAUDE.md's "Writing SKILL templates" section.
+
To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot flag, add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts`. Then rebuild.
## Conductor workspaces
diff --git a/VERSION b/VERSION
index 267577d..2b7c5ae 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.4.1
+0.4.2
diff --git a/gstack-upgrade/SKILL.md b/gstack-upgrade/SKILL.md
index 1cf7d54..42f13f6 100644
--- a/gstack-upgrade/SKILL.md
+++ b/gstack-upgrade/SKILL.md
@@ -94,14 +94,20 @@ fi
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
```
+The install type and directory path printed above will be used in all subsequent steps.
+
### Step 3: Save old version
+Use the install directory from Step 2's output below:
+
```bash
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
```
### Step 4: Upgrade
+Use the install type and directory detected in Step 2:
+
**For git installs** (global-git, local-git):
```bash
cd "$INSTALL_DIR"
@@ -125,7 +131,7 @@ rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
### Step 4.5: Sync local vendored copy
-After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
+Use the install directory from Step 2. Check if there's also a local vendored copy that needs updating:
```bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
diff --git a/gstack-upgrade/SKILL.md.tmpl b/gstack-upgrade/SKILL.md.tmpl
index 4a124be..a199db6 100644
--- a/gstack-upgrade/SKILL.md.tmpl
+++ b/gstack-upgrade/SKILL.md.tmpl
@@ -92,14 +92,20 @@ fi
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
```
+The install type and directory path printed above will be used in all subsequent steps.
+
### Step 3: Save old version
+Use the install directory from Step 2's output below:
+
```bash
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
```
### Step 4: Upgrade
+Use the install type and directory detected in Step 2:
+
**For git installs** (global-git, local-git):
```bash
cd "$INSTALL_DIR"
@@ -123,7 +129,7 @@ rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
### Step 4.5: Sync local vendored copy
-After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
+Use the install directory from Step 2. Check if there's also a local vendored copy that needs updating:
```bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md
index c82753a..77ca643 100644
--- a/plan-ceo-review/SKILL.md
+++ b/plan-ceo-review/SKILL.md
@@ -73,6 +73,25 @@ Then run: `mkdir -p ~/.gstack/contributor-logs && open ~/.gstack/contributor-log
Slug: lowercase, hyphens, max 60 chars (e.g. `browse-snapshot-ref-gap`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"
+## Step 0: Detect base branch
+
+Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
+
+1. Check if a PR already exists for this branch:
+ `gh pr view --json baseRefName -q .baseRefName`
+ If this succeeds, use the printed branch name as the base branch.
+
+2. If no PR exists (command fails), detect the repo's default branch:
+ `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+3. If both commands fail, fall back to `main`.
+
+Print the detected base branch name. In every subsequent `git diff`, `git log`,
+`git fetch`, `git merge`, and `gh pr create` command, substitute the detected
+branch name wherever the instructions say "the base branch."
+
+---
+
# Mega Plan Review Mode
## Philosophy
@@ -117,7 +136,7 @@ Before doing anything else, run a system audit. This is not the plan review —
Run the following commands:
```
git log --oneline -30 # Recent history
-git diff main --stat # What's already changed
+git diff --stat # What's already changed
git stash list # Any stashed work
grep -r "TODO\|FIXME\|HACK\|XXX" --include="*.rb" --include="*.js" -l
find . -name "*.rb" -newer Gemfile.lock | head -20 # Recently touched files
diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl
index ef14a28..9902faf 100644
--- a/plan-ceo-review/SKILL.md.tmpl
+++ b/plan-ceo-review/SKILL.md.tmpl
@@ -16,6 +16,8 @@ allowed-tools:
{{PREAMBLE}}
+{{BASE_BRANCH_DETECT}}
+
# Mega Plan Review Mode
## Philosophy
@@ -60,7 +62,7 @@ Before doing anything else, run a system audit. This is not the plan review —
Run the following commands:
```
git log --oneline -30 # Recent history
-git diff main --stat # What's already changed
+git diff --stat # What's already changed
git stash list # Any stashed work
grep -r "TODO\|FIXME\|HACK\|XXX" --include="*.rb" --include="*.js" -l
find . -name "*.rb" -newer Gemfile.lock | head -20 # Recently touched files
diff --git a/qa/SKILL.md b/qa/SKILL.md
index c11f8a6..5ea4643 100644
--- a/qa/SKILL.md
+++ b/qa/SKILL.md
@@ -77,6 +77,25 @@ Then run: `mkdir -p ~/.gstack/contributor-logs && open ~/.gstack/contributor-log
Slug: lowercase, hyphens, max 60 chars (e.g. `browse-snapshot-ref-gap`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"
+## Step 0: Detect base branch
+
+Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
+
+1. Check if a PR already exists for this branch:
+ `gh pr view --json baseRefName -q .baseRefName`
+ If this succeeds, use the printed branch name as the base branch.
+
+2. If no PR exists (command fails), detect the repo's default branch:
+ `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+3. If both commands fail, fall back to `main`.
+
+Print the detected base branch name. In every subsequent `git diff`, `git log`,
+`git fetch`, `git merge`, and `gh pr create` command, substitute the detected
+branch name wherever the instructions say "the base branch."
+
+---
+
# /qa: Test → Fix → Verify
You are a QA engineer AND a bug-fix engineer. Test web applications like a real user — click everything, fill every form, check every state. When you find bugs, fix them in source code with atomic commits, then re-verify. Produce a structured report with before/after evidence.
@@ -133,8 +152,7 @@ If `NEEDS_SETUP`:
**Create output directories:**
```bash
-REPORT_DIR=".gstack/qa-reports"
-mkdir -p "$REPORT_DIR/screenshots"
+mkdir -p .gstack/qa-reports/screenshots
```
---
diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl
index a3e5a9f..f491999 100644
--- a/qa/SKILL.md.tmpl
+++ b/qa/SKILL.md.tmpl
@@ -20,6 +20,8 @@ allowed-tools:
{{PREAMBLE}}
+{{BASE_BRANCH_DETECT}}
+
# /qa: Test → Fix → Verify
You are a QA engineer AND a bug-fix engineer. Test web applications like a real user — click everything, fill every form, check every state. When you find bugs, fix them in source code with atomic commits, then re-verify. Produce a structured report with before/after evidence.
@@ -59,8 +61,7 @@ fi
**Create output directories:**
```bash
-REPORT_DIR=".gstack/qa-reports"
-mkdir -p "$REPORT_DIR/screenshots"
+mkdir -p .gstack/qa-reports/screenshots
```
---
diff --git a/retro/SKILL.md b/retro/SKILL.md
index 28280c9..ca44f49 100644
--- a/retro/SKILL.md
+++ b/retro/SKILL.md
@@ -72,6 +72,16 @@ Then run: `mkdir -p ~/.gstack/contributor-logs && open ~/.gstack/contributor-log
Slug: lowercase, hyphens, max 60 chars (e.g. `browse-snapshot-ref-gap`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"
+## Detect default branch
+
+Before gathering data, detect the repo's default branch name:
+`gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+If this fails, fall back to `main`. Use the detected name wherever the instructions
+say `origin/` below.
+
+---
+
# /retro — Weekly Engineering Retrospective
Generates a comprehensive engineering retrospective analyzing commit history, work patterns, and code quality metrics. Team-aware: identifies the user running the command, then analyzes every contributor with per-person praise and growth opportunities. Designed for a senior IC/CTO-level builder using Claude Code as a force multiplier.
@@ -106,7 +116,7 @@ Usage: /retro [window]
First, fetch origin and identify the current user:
```bash
-git fetch origin main --quiet
+git fetch origin --quiet
# Identify who is running the retro
git config user.name
git config user.email
@@ -118,28 +128,28 @@ Run ALL of these git commands in parallel (they are independent):
```bash
# 1. All commits in window with timestamps, subject, hash, AUTHOR, files changed, insertions, deletions
-git log origin/main --since="" --format="%H|%aN|%ae|%ai|%s" --shortstat
+git log origin/ --since="" --format="%H|%aN|%ae|%ai|%s" --shortstat
# 2. Per-commit test vs total LOC breakdown with author
# Each commit block starts with COMMIT:|, followed by numstat lines.
# Separate test files (matching test/|spec/|__tests__/) from production files.
-git log origin/main --since="" --format="COMMIT:%H|%aN" --numstat
+git log origin/ --since="" --format="COMMIT:%H|%aN" --numstat
# 3. Commit timestamps for session detection and hourly distribution (with author)
# Use TZ=America/Los_Angeles for Pacific time conversion
-TZ=America/Los_Angeles git log origin/main --since="" --format="%at|%aN|%ai|%s" | sort -n
+TZ=America/Los_Angeles git log origin/ --since="" --format="%at|%aN|%ai|%s" | sort -n
# 4. Files most frequently changed (hotspot analysis)
-git log origin/main --since="" --format="" --name-only | grep -v '^$' | sort | uniq -c | sort -rn
+git log origin/ --since="" --format="" --name-only | grep -v '^$' | sort | uniq -c | sort -rn
# 5. PR numbers from commit messages (extract #NNN patterns)
-git log origin/main --since="" --format="%s" | grep -oE '#[0-9]+' | sed 's/^#//' | sort -n | uniq | sed 's/^/#/'
+git log origin/ --since="" --format="%s" | grep -oE '#[0-9]+' | sed 's/^#//' | sort -n | uniq | sed 's/^/#/'
# 6. Per-author file hotspots (who touches what)
-git log origin/main --since="" --format="AUTHOR:%aN" --name-only
+git log origin/ --since="" --format="AUTHOR:%aN" --name-only
# 7. Per-author commit counts (quick summary)
-git shortlog origin/main --since="" -sn --no-merges
+git shortlog origin/ --since="" -sn --no-merges
# 8. Greptile triage history (if available)
cat ~/.gstack/greptile-history.md 2>/dev/null || true
@@ -298,14 +308,14 @@ If the time window is 14 days or more, split into weekly buckets and show trends
### Step 11: Streak Tracking
-Count consecutive days with at least 1 commit to origin/main, going back from today. Track both team streak and personal streak:
+Count consecutive days with at least 1 commit to origin/, going back from today. Track both team streak and personal streak:
```bash
# Team streak: all unique commit dates (Pacific time) — no hard cutoff
-TZ=America/Los_Angeles git log origin/main --format="%ad" --date=format:"%Y-%m-%d" | sort -u
+TZ=America/Los_Angeles git log origin/ --format="%ad" --date=format:"%Y-%m-%d" | sort -u
# Personal streak: only the current user's commits
-TZ=America/Los_Angeles git log origin/main --author="" --format="%ad" --date=format:"%Y-%m-%d" | sort -u
+TZ=America/Los_Angeles git log origin/ --author="" --format="%ad" --date=format:"%Y-%m-%d" | sort -u
```
Count backward from today — how many consecutive days have at least one commit? This queries the full history so streaks of any length are reported accurately. Display both:
@@ -523,7 +533,7 @@ When the user runs `/retro compare` (or `/retro compare 14d`):
## Important Rules
- ALL narrative output goes directly to the user in the conversation. The ONLY file written is the `.context/retros/` JSON snapshot.
-- Use `origin/main` for all git queries (not local main which may be stale)
+- Use `origin/` for all git queries (not local main which may be stale)
- Convert all timestamps to Pacific time for display (use `TZ=America/Los_Angeles`)
- If the window has zero commits, say so and suggest a different window
- Round LOC/hour to nearest 50
diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl
index 07e0888..2f39fb5 100644
--- a/retro/SKILL.md.tmpl
+++ b/retro/SKILL.md.tmpl
@@ -15,6 +15,16 @@ allowed-tools:
{{PREAMBLE}}
+## Detect default branch
+
+Before gathering data, detect the repo's default branch name:
+`gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+If this fails, fall back to `main`. Use the detected name wherever the instructions
+say `origin/` below.
+
+---
+
# /retro — Weekly Engineering Retrospective
Generates a comprehensive engineering retrospective analyzing commit history, work patterns, and code quality metrics. Team-aware: identifies the user running the command, then analyzes every contributor with per-person praise and growth opportunities. Designed for a senior IC/CTO-level builder using Claude Code as a force multiplier.
@@ -49,7 +59,7 @@ Usage: /retro [window]
First, fetch origin and identify the current user:
```bash
-git fetch origin main --quiet
+git fetch origin --quiet
# Identify who is running the retro
git config user.name
git config user.email
@@ -61,28 +71,28 @@ Run ALL of these git commands in parallel (they are independent):
```bash
# 1. All commits in window with timestamps, subject, hash, AUTHOR, files changed, insertions, deletions
-git log origin/main --since="" --format="%H|%aN|%ae|%ai|%s" --shortstat
+git log origin/ --since="" --format="%H|%aN|%ae|%ai|%s" --shortstat
# 2. Per-commit test vs total LOC breakdown with author
# Each commit block starts with COMMIT:|, followed by numstat lines.
# Separate test files (matching test/|spec/|__tests__/) from production files.
-git log origin/main --since="" --format="COMMIT:%H|%aN" --numstat
+git log origin/ --since="" --format="COMMIT:%H|%aN" --numstat
# 3. Commit timestamps for session detection and hourly distribution (with author)
# Use TZ=America/Los_Angeles for Pacific time conversion
-TZ=America/Los_Angeles git log origin/main --since="" --format="%at|%aN|%ai|%s" | sort -n
+TZ=America/Los_Angeles git log origin/ --since="" --format="%at|%aN|%ai|%s" | sort -n
# 4. Files most frequently changed (hotspot analysis)
-git log origin/main --since="" --format="" --name-only | grep -v '^$' | sort | uniq -c | sort -rn
+git log origin/ --since="" --format="" --name-only | grep -v '^$' | sort | uniq -c | sort -rn
# 5. PR numbers from commit messages (extract #NNN patterns)
-git log origin/main --since="" --format="%s" | grep -oE '#[0-9]+' | sed 's/^#//' | sort -n | uniq | sed 's/^/#/'
+git log origin/ --since="" --format="%s" | grep -oE '#[0-9]+' | sed 's/^#//' | sort -n | uniq | sed 's/^/#/'
# 6. Per-author file hotspots (who touches what)
-git log origin/main --since="" --format="AUTHOR:%aN" --name-only
+git log origin/ --since="" --format="AUTHOR:%aN" --name-only
# 7. Per-author commit counts (quick summary)
-git shortlog origin/main --since="" -sn --no-merges
+git shortlog origin/ --since="" -sn --no-merges
# 8. Greptile triage history (if available)
cat ~/.gstack/greptile-history.md 2>/dev/null || true
@@ -241,14 +251,14 @@ If the time window is 14 days or more, split into weekly buckets and show trends
### Step 11: Streak Tracking
-Count consecutive days with at least 1 commit to origin/main, going back from today. Track both team streak and personal streak:
+Count consecutive days with at least 1 commit to origin/, going back from today. Track both team streak and personal streak:
```bash
# Team streak: all unique commit dates (Pacific time) — no hard cutoff
-TZ=America/Los_Angeles git log origin/main --format="%ad" --date=format:"%Y-%m-%d" | sort -u
+TZ=America/Los_Angeles git log origin/ --format="%ad" --date=format:"%Y-%m-%d" | sort -u
# Personal streak: only the current user's commits
-TZ=America/Los_Angeles git log origin/main --author="" --format="%ad" --date=format:"%Y-%m-%d" | sort -u
+TZ=America/Los_Angeles git log origin/ --author="" --format="%ad" --date=format:"%Y-%m-%d" | sort -u
```
Count backward from today — how many consecutive days have at least one commit? This queries the full history so streaks of any length are reported accurately. Display both:
@@ -466,7 +476,7 @@ When the user runs `/retro compare` (or `/retro compare 14d`):
## Important Rules
- ALL narrative output goes directly to the user in the conversation. The ONLY file written is the `.context/retros/` JSON snapshot.
-- Use `origin/main` for all git queries (not local main which may be stale)
+- Use `origin/` for all git queries (not local main which may be stale)
- Convert all timestamps to Pacific time for display (use `TZ=America/Los_Angeles`)
- If the window has zero commits, say so and suggest a different window
- Round LOC/hour to nearest 50
diff --git a/review/SKILL.md b/review/SKILL.md
index 32c597a..949a0c6 100644
--- a/review/SKILL.md
+++ b/review/SKILL.md
@@ -2,7 +2,7 @@
name: review
version: 1.0.0
description: |
- Pre-landing PR review. Analyzes diff against main for SQL safety, LLM trust
+ Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust
boundary violations, conditional side effects, and other structural issues.
allowed-tools:
- Bash
@@ -73,17 +73,36 @@ Then run: `mkdir -p ~/.gstack/contributor-logs && open ~/.gstack/contributor-log
Slug: lowercase, hyphens, max 60 chars (e.g. `browse-snapshot-ref-gap`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"
+## Step 0: Detect base branch
+
+Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
+
+1. Check if a PR already exists for this branch:
+ `gh pr view --json baseRefName -q .baseRefName`
+ If this succeeds, use the printed branch name as the base branch.
+
+2. If no PR exists (command fails), detect the repo's default branch:
+ `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+3. If both commands fail, fall back to `main`.
+
+Print the detected base branch name. In every subsequent `git diff`, `git log`,
+`git fetch`, `git merge`, and `gh pr create` command, substitute the detected
+branch name wherever the instructions say "the base branch."
+
+---
+
# Pre-Landing PR Review
-You are running the `/review` workflow. Analyze the current branch's diff against main for structural issues that tests don't catch.
+You are running the `/review` workflow. Analyze the current branch's diff against the base branch for structural issues that tests don't catch.
---
## Step 1: Check branch
1. Run `git branch --show-current` to get the current branch.
-2. If on `main`, output: **"Nothing to review — you're on main or have no changes against main."** and stop.
-3. Run `git fetch origin main --quiet && git diff origin/main --stat` to check if there's a diff. If no diff, output the same message and stop.
+2. If on the base branch, output: **"Nothing to review — you're on the base branch or have no changes against it."** and stop.
+3. Run `git fetch origin --quiet && git diff origin/ --stat` to check if there's a diff. If no diff, output the same message and stop.
---
@@ -107,13 +126,13 @@ Read `.claude/skills/review/greptile-triage.md` and follow the fetch, filter, cl
## Step 3: Get the diff
-Fetch the latest main to avoid false positives from a stale local main:
+Fetch the latest base branch to avoid false positives from stale local state:
```bash
-git fetch origin main --quiet
+git fetch origin --quiet
```
-Run `git diff origin/main` to get the full diff. This includes both committed and uncommitted changes against the latest main.
+Run `git diff origin/` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
---
diff --git a/review/SKILL.md.tmpl b/review/SKILL.md.tmpl
index 124a539..dadd211 100644
--- a/review/SKILL.md.tmpl
+++ b/review/SKILL.md.tmpl
@@ -2,7 +2,7 @@
name: review
version: 1.0.0
description: |
- Pre-landing PR review. Analyzes diff against main for SQL safety, LLM trust
+ Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust
boundary violations, conditional side effects, and other structural issues.
allowed-tools:
- Bash
@@ -16,17 +16,19 @@ allowed-tools:
{{PREAMBLE}}
+{{BASE_BRANCH_DETECT}}
+
# Pre-Landing PR Review
-You are running the `/review` workflow. Analyze the current branch's diff against main for structural issues that tests don't catch.
+You are running the `/review` workflow. Analyze the current branch's diff against the base branch for structural issues that tests don't catch.
---
## Step 1: Check branch
1. Run `git branch --show-current` to get the current branch.
-2. If on `main`, output: **"Nothing to review — you're on main or have no changes against main."** and stop.
-3. Run `git fetch origin main --quiet && git diff origin/main --stat` to check if there's a diff. If no diff, output the same message and stop.
+2. If on the base branch, output: **"Nothing to review — you're on the base branch or have no changes against it."** and stop.
+3. Run `git fetch origin --quiet && git diff origin/ --stat` to check if there's a diff. If no diff, output the same message and stop.
---
@@ -50,13 +52,13 @@ Read `.claude/skills/review/greptile-triage.md` and follow the fetch, filter, cl
## Step 3: Get the diff
-Fetch the latest main to avoid false positives from a stale local main:
+Fetch the latest base branch to avoid false positives from stale local state:
```bash
-git fetch origin main --quiet
+git fetch origin --quiet
```
-Run `git diff origin/main` to get the full diff. This includes both committed and uncommitted changes against the latest main.
+Run `git diff origin/` to get the full diff. This includes both committed and uncommitted changes against the latest base branch.
---
diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts
index bafed64..9c81e96 100644
--- a/scripts/gen-skill-docs.ts
+++ b/scripts/gen-skill-docs.ts
@@ -174,6 +174,27 @@ If \`NEEDS_SETUP\`:
3. If \`bun\` is not installed: \`curl -fsSL https://bun.sh/install | bash\``;
}
+function generateBaseBranchDetect(): string {
+ return `## Step 0: Detect base branch
+
+Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
+
+1. Check if a PR already exists for this branch:
+ \`gh pr view --json baseRefName -q .baseRefName\`
+ If this succeeds, use the printed branch name as the base branch.
+
+2. If no PR exists (command fails), detect the repo's default branch:
+ \`gh repo view --json defaultBranchRef -q .defaultBranchRef.name\`
+
+3. If both commands fail, fall back to \`main\`.
+
+Print the detected base branch name. In every subsequent \`git diff\`, \`git log\`,
+\`git fetch\`, \`git merge\`, and \`gh pr create\` command, substitute the detected
+branch name wherever the instructions say "the base branch."
+
+---`;
+}
+
function generateQAMethodology(): string {
return `## Modes
@@ -455,6 +476,7 @@ const RESOLVERS: Record string> = {
SNAPSHOT_FLAGS: generateSnapshotFlags,
PREAMBLE: generatePreamble,
BROWSE_SETUP: generateBrowseSetup,
+ BASE_BRANCH_DETECT: generateBaseBranchDetect,
QA_METHODOLOGY: generateQAMethodology,
};
diff --git a/ship/SKILL.md b/ship/SKILL.md
index e023816..47a9da1 100644
--- a/ship/SKILL.md
+++ b/ship/SKILL.md
@@ -2,7 +2,7 @@
name: ship
version: 1.0.0
description: |
- Ship workflow: merge main, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR.
+ Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR.
allowed-tools:
- Bash
- Read
@@ -72,12 +72,31 @@ Then run: `mkdir -p ~/.gstack/contributor-logs && open ~/.gstack/contributor-log
Slug: lowercase, hyphens, max 60 chars (e.g. `browse-snapshot-ref-gap`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"
+## Step 0: Detect base branch
+
+Determine which branch this PR targets. Use the result as "the base branch" in all subsequent steps.
+
+1. Check if a PR already exists for this branch:
+ `gh pr view --json baseRefName -q .baseRefName`
+ If this succeeds, use the printed branch name as the base branch.
+
+2. If no PR exists (command fails), detect the repo's default branch:
+ `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`
+
+3. If both commands fail, fall back to `main`.
+
+Print the detected base branch name. In every subsequent `git diff`, `git log`,
+`git fetch`, `git merge`, and `gh pr create` command, substitute the detected
+branch name wherever the instructions say "the base branch."
+
+---
+
# Ship: Fully Automated Ship Workflow
You are running the `/ship` workflow. This is a **non-interactive, fully automated** workflow. Do NOT ask for confirmation at any step. The user said `/ship` which means DO IT. Run straight through and output the PR URL at the end.
**Only stop for:**
-- On `main` branch (abort)
+- On the base branch (abort)
- Merge conflicts that can't be auto-resolved (stop, show conflicts)
- Test failures (stop, show failures)
- Pre-landing review finds CRITICAL issues and user chooses to fix (not acknowledge or skip)
@@ -98,20 +117,20 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
## Step 1: Pre-flight
-1. Check the current branch. If on `main`, **abort**: "You're on main. Ship from a feature branch."
+1. Check the current branch. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
-3. Run `git diff main...HEAD --stat` and `git log main..HEAD --oneline` to understand what's being shipped.
+3. Run `git diff ...HEAD --stat` and `git log ..HEAD --oneline` to understand what's being shipped.
---
-## Step 2: Merge origin/main (BEFORE tests)
+## Step 2: Merge the base branch (BEFORE tests)
-Fetch and merge `origin/main` into the feature branch so tests run against the merged state:
+Fetch and merge the base branch into the feature branch so tests run against the merged state:
```bash
-git fetch origin main && git merge origin/main --no-edit
+git fetch origin && git merge origin/ --no-edit
```
**If there are merge conflicts:** Try to auto-resolve if they are simple (VERSION, schema.rb, CHANGELOG ordering). If conflicts are complex or ambiguous, **STOP** and show them.
@@ -149,7 +168,7 @@ Evals are mandatory when prompt-related files change. Skip this step entirely if
**1. Check if the diff touches prompt-related files:**
```bash
-git diff origin/main --name-only
+git diff origin/ --name-only
```
Match against these patterns (from CLAUDE.md):
@@ -210,7 +229,7 @@ Review the diff for structural issues that tests don't catch.
1. Read `.claude/skills/review/checklist.md`. If the file cannot be read, **STOP** and report the error.
-2. Run `git diff origin/main` to get the full diff (scoped to feature changes against the freshly-fetched remote main).
+2. Run `git diff origin/` to get the full diff (scoped to feature changes against the freshly-fetched base branch).
3. Apply the review checklist in two passes:
- **Pass 1 (CRITICAL):** SQL & Data Safety, LLM Output Trust Boundary
@@ -278,7 +297,7 @@ For each classified comment:
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
2. **Auto-decide the bump level based on the diff:**
- - Count lines changed (`git diff origin/main...HEAD --stat | tail -1`)
+ - Count lines changed (`git diff origin/...HEAD --stat | tail -1`)
- **MICRO** (4th digit): < 50 lines changed, trivial tweaks, typos, config
- **PATCH** (3rd digit): 50+ lines changed, bug fixes, small-medium features
- **MINOR** (2nd digit): **ASK the user** — only for major features or significant architectural changes
@@ -297,8 +316,8 @@ For each classified comment:
1. Read `CHANGELOG.md` header to know the format.
2. Auto-generate the entry from **ALL commits on the branch** (not just recent ones):
- - Use `git log main..HEAD --oneline` to see every commit being shipped
- - Use `git diff main...HEAD` to see the full diff against main
+ - Use `git log ..HEAD --oneline` to see every commit being shipped
+ - Use `git diff ...HEAD` to see the full diff against the base branch
- The CHANGELOG entry must be comprehensive of ALL changes going into the PR
- If existing CHANGELOG entries on the branch already cover some commits, replace them with one unified entry for the new version
- Categorize changes into applicable sections:
@@ -346,8 +365,8 @@ Read TODOS.md and verify it follows the recommended structure:
This step is fully automatic — no user interaction.
Use the diff and commit history already gathered in earlier steps:
-- `git diff main...HEAD` (full diff against main)
-- `git log main..HEAD --oneline` (all commits being shipped)
+- `git diff ...HEAD` (full diff against the base branch)
+- `git log ..HEAD --oneline` (all commits being shipped)
For each TODO item, check if the changes in this PR complete it by:
- Matching commit messages against the TODO title and description
@@ -422,7 +441,7 @@ git push -u origin
Create a pull request using `gh`:
```bash
-gh pr create --title ": " --body "$(cat <<'EOF'
+gh pr create --base --title ": " --body "$(cat <<'EOF'
## Summary
diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl
index 06ff5a0..ae5df40 100644
--- a/ship/SKILL.md.tmpl
+++ b/ship/SKILL.md.tmpl
@@ -2,7 +2,7 @@
name: ship
version: 1.0.0
description: |
- Ship workflow: merge main, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR.
+ Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR.
allowed-tools:
- Bash
- Read
@@ -15,12 +15,14 @@ allowed-tools:
{{PREAMBLE}}
+{{BASE_BRANCH_DETECT}}
+
# Ship: Fully Automated Ship Workflow
You are running the `/ship` workflow. This is a **non-interactive, fully automated** workflow. Do NOT ask for confirmation at any step. The user said `/ship` which means DO IT. Run straight through and output the PR URL at the end.
**Only stop for:**
-- On `main` branch (abort)
+- On the base branch (abort)
- Merge conflicts that can't be auto-resolved (stop, show conflicts)
- Test failures (stop, show failures)
- Pre-landing review finds CRITICAL issues and user chooses to fix (not acknowledge or skip)
@@ -41,20 +43,20 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat
## Step 1: Pre-flight
-1. Check the current branch. If on `main`, **abort**: "You're on main. Ship from a feature branch."
+1. Check the current branch. If on the base branch or the repo's default branch, **abort**: "You're on the base branch. Ship from a feature branch."
2. Run `git status` (never use `-uall`). Uncommitted changes are always included — no need to ask.
-3. Run `git diff main...HEAD --stat` and `git log main..HEAD --oneline` to understand what's being shipped.
+3. Run `git diff ...HEAD --stat` and `git log ..HEAD --oneline` to understand what's being shipped.
---
-## Step 2: Merge origin/main (BEFORE tests)
+## Step 2: Merge the base branch (BEFORE tests)
-Fetch and merge `origin/main` into the feature branch so tests run against the merged state:
+Fetch and merge the base branch into the feature branch so tests run against the merged state:
```bash
-git fetch origin main && git merge origin/main --no-edit
+git fetch origin && git merge origin/ --no-edit
```
**If there are merge conflicts:** Try to auto-resolve if they are simple (VERSION, schema.rb, CHANGELOG ordering). If conflicts are complex or ambiguous, **STOP** and show them.
@@ -92,7 +94,7 @@ Evals are mandatory when prompt-related files change. Skip this step entirely if
**1. Check if the diff touches prompt-related files:**
```bash
-git diff origin/main --name-only
+git diff origin/ --name-only
```
Match against these patterns (from CLAUDE.md):
@@ -153,7 +155,7 @@ Review the diff for structural issues that tests don't catch.
1. Read `.claude/skills/review/checklist.md`. If the file cannot be read, **STOP** and report the error.
-2. Run `git diff origin/main` to get the full diff (scoped to feature changes against the freshly-fetched remote main).
+2. Run `git diff origin/` to get the full diff (scoped to feature changes against the freshly-fetched base branch).
3. Apply the review checklist in two passes:
- **Pass 1 (CRITICAL):** SQL & Data Safety, LLM Output Trust Boundary
@@ -221,7 +223,7 @@ For each classified comment:
1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`)
2. **Auto-decide the bump level based on the diff:**
- - Count lines changed (`git diff origin/main...HEAD --stat | tail -1`)
+ - Count lines changed (`git diff origin/...HEAD --stat | tail -1`)
- **MICRO** (4th digit): < 50 lines changed, trivial tweaks, typos, config
- **PATCH** (3rd digit): 50+ lines changed, bug fixes, small-medium features
- **MINOR** (2nd digit): **ASK the user** — only for major features or significant architectural changes
@@ -240,8 +242,8 @@ For each classified comment:
1. Read `CHANGELOG.md` header to know the format.
2. Auto-generate the entry from **ALL commits on the branch** (not just recent ones):
- - Use `git log main..HEAD --oneline` to see every commit being shipped
- - Use `git diff main...HEAD` to see the full diff against main
+ - Use `git log ..HEAD --oneline` to see every commit being shipped
+ - Use `git diff ...HEAD` to see the full diff against the base branch
- The CHANGELOG entry must be comprehensive of ALL changes going into the PR
- If existing CHANGELOG entries on the branch already cover some commits, replace them with one unified entry for the new version
- Categorize changes into applicable sections:
@@ -289,8 +291,8 @@ Read TODOS.md and verify it follows the recommended structure:
This step is fully automatic — no user interaction.
Use the diff and commit history already gathered in earlier steps:
-- `git diff main...HEAD` (full diff against main)
-- `git log main..HEAD --oneline` (all commits being shipped)
+- `git diff ...HEAD` (full diff against the base branch)
+- `git log ..HEAD --oneline` (all commits being shipped)
For each TODO item, check if the changes in this PR complete it by:
- Matching commit messages against the TODO title and description
@@ -365,7 +367,7 @@ git push -u origin
Create a pull request using `gh`:
```bash
-gh pr create --title ": " --body "$(cat <<'EOF'
+gh pr create --base --title ": " --body "$(cat <<'EOF'
## Summary
diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts
index a2499af..e77989f 100644
--- a/test/gen-skill-docs.test.ts
+++ b/test/gen-skill-docs.test.ts
@@ -203,6 +203,27 @@ describe('gen-skill-docs', () => {
});
});
+describe('BASE_BRANCH_DETECT resolver', () => {
+ // Find a generated SKILL.md that uses the placeholder (ship is guaranteed to)
+ const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
+
+ test('resolver output contains PR base detection command', () => {
+ expect(shipContent).toContain('gh pr view --json baseRefName');
+ });
+
+ test('resolver output contains repo default branch detection command', () => {
+ expect(shipContent).toContain('gh repo view --json defaultBranchRef');
+ });
+
+ test('resolver output contains fallback to main', () => {
+ expect(shipContent).toMatch(/fall\s*back\s+to\s+`main`/i);
+ });
+
+ test('resolver output uses "the base branch" phrasing', () => {
+ expect(shipContent).toContain('the base branch');
+ });
+});
+
/**
* Quality evals — catch description regressions.
*
diff --git a/test/skill-e2e.test.ts b/test/skill-e2e.test.ts
index 8ce1f40..4978ce5 100644
--- a/test/skill-e2e.test.ts
+++ b/test/skill-e2e.test.ts
@@ -1344,6 +1344,193 @@ Write your review to ${planDir}/review-output.md`,
}, 420_000);
});
+// --- Base branch detection smoke tests ---
+
+describeE2E('Base branch detection', () => {
+ let baseBranchDir: string;
+ const run = (cmd: string, args: string[], cwd: string) =>
+ spawnSync(cmd, args, { cwd, stdio: 'pipe', timeout: 5000 });
+
+ beforeAll(() => {
+ baseBranchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-basebranch-'));
+ });
+
+ afterAll(() => {
+ try { fs.rmSync(baseBranchDir, { recursive: true, force: true }); } catch {}
+ });
+
+ test('/review detects base branch and diffs against it', async () => {
+ const dir = path.join(baseBranchDir, 'review-base');
+ fs.mkdirSync(dir, { recursive: true });
+
+ // Create git repo with a feature branch off main
+ run('git', ['init'], dir);
+ run('git', ['config', 'user.email', 'test@test.com'], dir);
+ run('git', ['config', 'user.name', 'Test'], dir);
+
+ fs.writeFileSync(path.join(dir, 'app.rb'), '# clean base\nclass App\nend\n');
+ run('git', ['add', 'app.rb'], dir);
+ run('git', ['commit', '-m', 'initial commit'], dir);
+
+ // Create feature branch with a change
+ run('git', ['checkout', '-b', 'feature/test-review'], dir);
+ fs.writeFileSync(path.join(dir, 'app.rb'), '# clean base\nclass App\n def hello; "world"; end\nend\n');
+ run('git', ['add', 'app.rb'], dir);
+ run('git', ['commit', '-m', 'feat: add hello method'], dir);
+
+ // Copy review skill files
+ fs.copyFileSync(path.join(ROOT, 'review', 'SKILL.md'), path.join(dir, 'review-SKILL.md'));
+ fs.copyFileSync(path.join(ROOT, 'review', 'checklist.md'), path.join(dir, 'review-checklist.md'));
+ fs.copyFileSync(path.join(ROOT, 'review', 'greptile-triage.md'), path.join(dir, 'review-greptile-triage.md'));
+
+ const result = await runSkillTest({
+ prompt: `You are in a git repo on a feature branch with changes.
+Read review-SKILL.md for the review workflow instructions.
+Also read review-checklist.md and apply it.
+
+IMPORTANT: Follow Step 0 to detect the base branch. Since there is no remote, gh commands will fail — fall back to main.
+Then run the review against the detected base branch.
+Write your findings to ${dir}/review-output.md`,
+ workingDirectory: dir,
+ maxTurns: 15,
+ timeout: 90_000,
+ testName: 'review-base-branch',
+ runId,
+ });
+
+ logCost('/review base-branch', result);
+ recordE2E('/review base branch detection', 'Base branch detection', result);
+ expect(result.exitReason).toBe('success');
+
+ // Verify the review used "base branch" language (from Step 0)
+ const toolOutputs = result.toolCalls.map(tc => tc.output || '').join('\n');
+ const allOutput = (result.output || '') + toolOutputs;
+ // The agent should have run git diff against main (the fallback)
+ const usedGitDiff = result.toolCalls.some(tc =>
+ tc.tool === 'Bash' && typeof tc.input === 'string' && tc.input.includes('git diff')
+ );
+ expect(usedGitDiff).toBe(true);
+ }, 120_000);
+
+ test('/ship Step 0-1 detects base branch without destructive actions', async () => {
+ const dir = path.join(baseBranchDir, 'ship-base');
+ fs.mkdirSync(dir, { recursive: true });
+
+ // Create git repo with feature branch
+ run('git', ['init'], dir);
+ run('git', ['config', 'user.email', 'test@test.com'], dir);
+ run('git', ['config', 'user.name', 'Test'], dir);
+
+ fs.writeFileSync(path.join(dir, 'app.ts'), 'console.log("v1");\n');
+ run('git', ['add', 'app.ts'], dir);
+ run('git', ['commit', '-m', 'initial'], dir);
+
+ run('git', ['checkout', '-b', 'feature/ship-test'], dir);
+ fs.writeFileSync(path.join(dir, 'app.ts'), 'console.log("v2");\n');
+ run('git', ['add', 'app.ts'], dir);
+ run('git', ['commit', '-m', 'feat: update to v2'], dir);
+
+ // Copy ship skill
+ fs.copyFileSync(path.join(ROOT, 'ship', 'SKILL.md'), path.join(dir, 'ship-SKILL.md'));
+
+ const result = await runSkillTest({
+ prompt: `Read ship-SKILL.md for the ship workflow.
+
+Run ONLY Step 0 (Detect base branch) and Step 1 (Pre-flight) from the ship workflow.
+Since there is no remote, gh commands will fail — fall back to main.
+
+After completing Step 0 and Step 1, STOP. Do NOT proceed to Step 2 or beyond.
+Do NOT push, create PRs, or modify VERSION/CHANGELOG.
+
+Write a summary of what you detected to ${dir}/ship-preflight.md including:
+- The detected base branch name
+- The current branch name
+- The diff stat against the base branch`,
+ workingDirectory: dir,
+ maxTurns: 10,
+ timeout: 60_000,
+ testName: 'ship-base-branch',
+ runId,
+ });
+
+ logCost('/ship base-branch', result);
+ recordE2E('/ship base branch detection', 'Base branch detection', result);
+ expect(result.exitReason).toBe('success');
+
+ // Verify preflight output was written
+ const preflightPath = path.join(dir, 'ship-preflight.md');
+ if (fs.existsSync(preflightPath)) {
+ const content = fs.readFileSync(preflightPath, 'utf-8');
+ expect(content.length).toBeGreaterThan(20);
+ // Should mention the branch name
+ expect(content.toLowerCase()).toMatch(/main|base/);
+ }
+
+ // Verify no destructive actions — no push, no PR creation
+ const destructiveTools = result.toolCalls.filter(tc =>
+ tc.tool === 'Bash' && typeof tc.input === 'string' &&
+ (tc.input.includes('git push') || tc.input.includes('gh pr create'))
+ );
+ expect(destructiveTools).toHaveLength(0);
+ }, 90_000);
+
+ test('/retro detects default branch for git queries', async () => {
+ const dir = path.join(baseBranchDir, 'retro-base');
+ fs.mkdirSync(dir, { recursive: true });
+
+ // Create git repo with commit history
+ run('git', ['init'], dir);
+ run('git', ['config', 'user.email', 'dev@example.com'], dir);
+ run('git', ['config', 'user.name', 'Dev'], dir);
+
+ fs.writeFileSync(path.join(dir, 'app.ts'), 'console.log("hello");\n');
+ run('git', ['add', 'app.ts'], dir);
+ run('git', ['commit', '-m', 'feat: initial app', '--date', '2026-03-14T09:00:00'], dir);
+
+ fs.writeFileSync(path.join(dir, 'auth.ts'), 'export function login() {}\n');
+ run('git', ['add', 'auth.ts'], dir);
+ run('git', ['commit', '-m', 'feat: add auth', '--date', '2026-03-15T10:00:00'], dir);
+
+ fs.writeFileSync(path.join(dir, 'test.ts'), 'test("it works", () => {});\n');
+ run('git', ['add', 'test.ts'], dir);
+ run('git', ['commit', '-m', 'test: add tests', '--date', '2026-03-16T11:00:00'], dir);
+
+ // Copy retro skill
+ fs.mkdirSync(path.join(dir, 'retro'), { recursive: true });
+ fs.copyFileSync(path.join(ROOT, 'retro', 'SKILL.md'), path.join(dir, 'retro', 'SKILL.md'));
+
+ const result = await runSkillTest({
+ prompt: `Read retro/SKILL.md for instructions on how to run a retrospective.
+
+IMPORTANT: Follow the "Detect default branch" step first. Since there is no remote, gh will fail — fall back to main.
+Then use the detected branch name for all git queries.
+
+Run /retro for the last 7 days of this git repo. Skip any AskUserQuestion calls — this is non-interactive.
+This is a local-only repo so use the local branch (main) instead of origin/main for all git log commands.
+
+Write your retrospective to ${dir}/retro-output.md`,
+ workingDirectory: dir,
+ maxTurns: 25,
+ timeout: 240_000,
+ testName: 'retro-base-branch',
+ runId,
+ });
+
+ logCost('/retro base-branch', result);
+ recordE2E('/retro default branch detection', 'Base branch detection', result, {
+ passed: ['success', 'error_max_turns'].includes(result.exitReason),
+ });
+ expect(['success', 'error_max_turns']).toContain(result.exitReason);
+
+ // Verify retro output was produced
+ const retroPath = path.join(dir, 'retro-output.md');
+ if (fs.existsSync(retroPath)) {
+ const content = fs.readFileSync(retroPath, 'utf-8');
+ expect(content.length).toBeGreaterThan(100);
+ }
+ }, 300_000);
+});
+
// --- Deferred skill E2E tests (destructive or require interactive UI) ---
describeE2E('Deferred skill E2E', () => {
diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts
index 88e9893..2a947b1 100644
--- a/test/skill-validation.test.ts
+++ b/test/skill-validation.test.ts
@@ -388,6 +388,64 @@ describe('Greptile history format consistency', () => {
});
});
+// --- Hardcoded branch name detection in templates ---
+
+describe('No hardcoded branch names in SKILL templates', () => {
+ const tmplFiles = [
+ 'ship/SKILL.md.tmpl',
+ 'review/SKILL.md.tmpl',
+ 'qa/SKILL.md.tmpl',
+ 'plan-ceo-review/SKILL.md.tmpl',
+ 'retro/SKILL.md.tmpl',
+ ];
+
+ // Patterns that indicate hardcoded 'main' in git commands
+ const gitMainPatterns = [
+ /\bgit\s+diff\s+(?:origin\/)?main\b/,
+ /\bgit\s+log\s+(?:origin\/)?main\b/,
+ /\bgit\s+fetch\s+origin\s+main\b/,
+ /\bgit\s+merge\s+origin\/main\b/,
+ /\borigin\/main\b/,
+ ];
+
+ // Lines that are allowed to mention 'main' (fallback logic, prose)
+ const allowlist = [
+ /fall\s*back\s+to\s+`main`/i,
+ /fall\s*back\s+to\s+`?main`?/i,
+ /typically\s+`?main`?/i,
+ /If\s+on\s+`main`/i, // old pattern — should not exist
+ ];
+
+ for (const tmplFile of tmplFiles) {
+ test(`${tmplFile} has no hardcoded 'main' in git commands`, () => {
+ const filePath = path.join(ROOT, tmplFile);
+ if (!fs.existsSync(filePath)) return;
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
+ const violations: string[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const isAllowlisted = allowlist.some(p => p.test(line));
+ if (isAllowlisted) continue;
+
+ for (const pattern of gitMainPatterns) {
+ if (pattern.test(line)) {
+ violations.push(`Line ${i + 1}: ${line.trim()}`);
+ break;
+ }
+ }
+ }
+
+ if (violations.length > 0) {
+ throw new Error(
+ `${tmplFile} has hardcoded 'main' in git commands:\n` +
+ violations.map(v => ` ${v}`).join('\n')
+ );
+ }
+ });
+ }
+});
+
// --- Part 7b: TODOS-format.md reference consistency ---
describe('TODOS-format.md reference consistency', () => {