merge: resolve conflicts with upstream main

VALID_CONFIG_KEYS: merge our additions (workflow.auto_advance,
workflow.node_repair, workflow.node_repair_budget, hooks.context_warnings)
with upstream's additions (workflow.text_mode, git.quick_branch_template).

ensureConfigFile(): keep our refactored version that delegates to
buildNewProjectConfig({}) instead of upstream's duplicated logic.

buildNewProjectConfig(): add git.quick_branch_template: null and
workflow.text_mode: false to match upstream's new keys.

new-project.md: integrate upstream's Step 5.1 Sub-Repo Detection
after our commit block; drop upstream's duplicate Note (ours at
line 493 is more detailed).
This commit is contained in:
Diego Mariño
2026-03-19 22:48:21 +01:00
120 changed files with 12116 additions and 790 deletions

View File

@@ -22,7 +22,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [18, 20, 22]
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -37,13 +37,5 @@ jobs:
run: npm ci
- name: Run tests with coverage
# c8 v11 requires Node 20+ (engines: ^20.0.0 || >=22.0.0). Node 18 EOL April 2025.
# Use bash on all platforms so shell glob expansion works on Windows.
if: matrix.node-version != 18
shell: bash
run: npm run test:coverage
- name: Run tests (Node 18, coverage not supported)
if: matrix.node-version == 18
shell: bash
run: npm test

51
.release-monitor.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Release monitor for gsd-build/get-shit-done
# Checks every 15 minutes, writes new release info to a signal file
REPO="gsd-build/get-shit-done"
SIGNAL_FILE="/tmp/gsd-new-release.json"
STATE_FILE="/tmp/gsd-monitor-last-tag"
LOG_FILE="/tmp/gsd-monitor.log"
# Initialize with current latest
echo "v1.25.1" > "$STATE_FILE"
rm -f "$SIGNAL_FILE"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log "Monitor started. Watching $REPO for releases newer than v1.25.1"
log "Checking every 15 minutes..."
while true; do
sleep 900 # 15 minutes
LAST_KNOWN=$(cat "$STATE_FILE" 2>/dev/null)
# Get latest release tag
LATEST=$(gh release list -R "$REPO" --limit 1 2>/dev/null | awk '{print $1}')
if [ -z "$LATEST" ]; then
log "WARNING: Failed to fetch releases (network issue?)"
continue
fi
if [ "$LATEST" != "$LAST_KNOWN" ]; then
log "NEW RELEASE DETECTED: $LATEST (was: $LAST_KNOWN)"
# Fetch release notes
RELEASE_BODY=$(gh release view "$LATEST" -R "$REPO" --json tagName,name,body 2>/dev/null)
# Write signal file for the agent to pick up
echo "$RELEASE_BODY" > "$SIGNAL_FILE"
echo "$LATEST" > "$STATE_FILE"
log "Signal file written to $SIGNAL_FILE"
# Exit so the agent can process it, then restart
exit 0
else
log "No new release. Latest is still $LATEST"
fi
done

View File

@@ -6,15 +6,51 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [1.26.0] - 2026-03-18
### Added
- **`/gsd:profile-user` command** — Developer behavioral profiling from session analysis across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section for personalized responses. Includes `--questionnaire` fallback and `--refresh` for re-analysis
- **Execution hardening** — Three quality improvements to the execution pipeline:
- Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave
- Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines
- Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called
- **Developer profiling pipeline** — `/gsd:profile-user` analyzes Claude Code session history to build behavioral profiles across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section. Includes `--questionnaire` fallback and `--refresh` for re-analysis (#1084)
- **`/gsd:ship` command** — PR creation from verified phase work. Auto-generates rich PR body from planning artifacts, pushes branch, creates PR via `gh`, and updates STATE.md (#829)
- **`/gsd:next` command** — Automatic workflow advancement to the next logical step (#927)
- **Cross-phase regression gate** — Execute-phase runs prior phases' test suites after execution, catching regressions before they compound (#945)
- **Requirements coverage gate** — Plan-phase verifies all phase requirements are covered by at least one plan before proceeding (#984)
- **Structured session handoff artifact** — `/gsd:pause-work` writes `.planning/HANDOFF.json` for machine-readable cross-session continuity (#940)
- **WAITING.json signal file** — Machine-readable signal for decision points requiring user input (#1034)
- **Interactive executor mode** — Pair-programming style execution with step-by-step user involvement (#963)
- **MCP tool awareness** — GSD subagents can discover and use MCP server tools (#973)
- **Codex hooks support** — SessionStart hook support for Codex runtime (#1020)
- **Model alias-to-full-ID resolution** — Task API compatibility for model alias strings (#991)
- **Execution hardening** — Pre-wave dependency checks, cross-plan data contracts, and export-level spot checks (#1082)
- **Markdown normalization** — Generated markdown conforms to markdownlint standards (#1112)
- **`/gsd:audit-uat` command** — Cross-phase audit of all outstanding UAT and verification items. Scans every phase for pending, skipped, blocked, and human_needed items. Cross-references against codebase to detect stale documentation. Produces prioritized human test plan grouped by testability
- **Verification debt tracking** — Five structural improvements to prevent silent loss of UAT/verification items when projects advance:
- Cross-phase health check in `/gsd:progress` (Step 1.6) surfaces outstanding items from ALL prior phases
- `status: partial` in UAT files distinguishes incomplete testing from completed sessions
- `result: blocked` with `blocked_by` tag for tests blocked by external dependencies (server, device, build, third-party)
- `human_needed` verification items now persist as HUMAN-UAT.md files (trackable across sessions)
- Phase completion and transition warnings surface verification debt non-blockingly
### Changed
- Test suite consolidated: runtime converters deduplicated, helpers standardized (#1169)
- Added test coverage for model-profiles, templates, profile-pipeline, profile-output (#1170)
- Documented `inherit` profile for non-Anthropic providers (#1036)
### Fixed
- **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948)
- Agent suggests non-existent `/gsd:transition` — replaced with real commands (#1081, #1100)
- PROJECT.md drift and phase completion counter accuracy (#956)
- Copilot executor stuck issue — runtime compatibility fallback added (#1128)
- Explicit agent type listings prevent fallback after `/clear` (#949)
- Nested Skill calls breaking AskUserQuestion (#1009)
- Negative-heuristic `stripShippedMilestones` replaced with positive milestone lookup (#1145)
- Hook version tracking, stale hook detection, stdin timeout, session-report command (#1153, #1157, #1161, #1162)
- Hook build script syntax validation (#1165)
- Verification examples use `fetch()` instead of `curl` for Windows compatibility (#899)
- Sequential fallback for `map-codebase` on runtimes without Task tool (#1174)
- Zsh word-splitting fix for RUNTIME_DIRS arrays (#1173)
- CRLF frontmatter parsing, duplicate cwd crash, STATE.md phase transitions (#1105)
- Requirements `mark-complete` made idempotent (#948)
- Profile template paths, field names, and evidence key corrections (#1095)
- Duplicate variable declaration removed (#1101)
## [1.25.0] - 2026-03-16
@@ -1536,7 +1572,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- YOLO mode for autonomous execution
- Interactive mode with checkpoints
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.25.0...HEAD
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.26.0...HEAD
[1.26.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.26.0
[1.25.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.25.0
[1.24.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.24.0
[1.23.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.23.0

View File

@@ -8,6 +8,8 @@
**Solves context rot — the quality degradation that happens as Claude fills its context window.**
[**English**](README.md) | [**简体中文**](docs/zh-CN/README.md)
[![npm version](https://img.shields.io/npm/v/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc)
[![npm downloads](https://img.shields.io/npm/dm/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc)
[![Tests](https://img.shields.io/github/actions/workflow/status/glittercowboy/get-shit-done/test.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml)
@@ -342,19 +344,26 @@ If everything passes, you move on. If something's broken, you don't manually deb
---
### 6. Repeat → Complete → Next Milestone
### 6. Repeat → Ship → Complete → Next Milestone
```
/gsd:discuss-phase 2
/gsd:plan-phase 2
/gsd:execute-phase 2
/gsd:verify-work 2
/gsd:ship 2 # Create PR from verified work
...
/gsd:complete-milestone
/gsd:new-milestone
```
Loop **discuss → plan → execute → verify** until milestone complete.
Or let GSD figure out the next step automatically:
```
/gsd:next # Auto-detect and run next step
```
Loop **discuss → plan → execute → verify → ship** until milestone complete.
If you want faster intake during discussion, use `/gsd:discuss-phase <n> --batch` to answer a small grouped set of questions at once instead of one-by-one.
@@ -491,6 +500,8 @@ You're never locked in. The system adapts.
| `/gsd:plan-phase [N] [--auto]` | Research + plan + verify for a phase |
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves, verify when complete |
| `/gsd:verify-work [N]` | Manual user acceptance testing ¹ |
| `/gsd:ship [N] [--draft]` | Create PR from verified phase work with auto-generated body |
| `/gsd:next` | Automatically advance to the next logical workflow step |
| `/gsd:audit-milestone` | Verify milestone achieved its definition of done |
| `/gsd:complete-milestone` | Archive milestone, tag release |
| `/gsd:new-milestone [name]` | Start next version: questions → research → requirements → roadmap |
@@ -507,6 +518,7 @@ You're never locked in. The system adapts.
| Command | What it does |
|---------|--------------|
| `/gsd:progress` | Where am I? What's next? |
| `/gsd:next` | Auto-detect state and run the next step |
| `/gsd:help` | Show all commands and usage guide |
| `/gsd:update` | Update GSD with changelog preview |
| `/gsd:join-discord` | Join the GSD Discord community |
@@ -531,8 +543,9 @@ You're never locked in. The system adapts.
| Command | What it does |
|---------|--------------|
| `/gsd:pause-work` | Create handoff when stopping mid-phase |
| `/gsd:pause-work` | Create handoff when stopping mid-phase (writes HANDOFF.json) |
| `/gsd:resume-work` | Restore from last session |
| `/gsd:session-report` | Generate session summary with work performed and outcomes |
### Utilities
@@ -581,7 +594,7 @@ Switch profiles:
/gsd:set-profile budget
```
Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`).
Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`).
Or configure via `/gsd:settings`.

View File

@@ -47,7 +47,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `plans`, `incomplete_plans`.
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `plans`, `incomplete_plans`.
Also read STATE.md for position, decisions, blockers:
```bash
@@ -328,6 +328,14 @@ git add src/types/user.ts
| `chore` | Config, tooling, dependencies |
**4. Commit:**
**If `sub_repos` is configured (non-empty array from init context):** Use `commit-to-subrepo` to route files to their correct sub-repo:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {concise task description}" --files file1 file2 ...
```
Returns JSON with per-repo commit hashes: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`. Record all hashes for SUMMARY.
**Otherwise (standard single-repo):**
```bash
git commit -m "{type}({phase}-{plan}): {concise task description}
@@ -336,7 +344,9 @@ git commit -m "{type}({phase}-{plan}): {concise task description}
"
```
**5. Record hash:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY.
**5. Record hash:**
- **Single-repo:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY.
- **Multi-repo (sub_repos):** Extract hashes from `commit-to-subrepo` JSON output (`repos.{name}.hash`). Record all hashes for SUMMARY (e.g., `backend@abc1234, frontend@def5678`).
**6. Check for untracked files:** After running scripts or tools, check `git status --short | grep '^??'`. For any new untracked files: commit if intentional, add to `.gitignore` if generated/runtime output. Never leave generated files untracked.
</task_commit_protocol>

View File

@@ -194,6 +194,7 @@ Priority: Context7 > Official Docs > Official GitHub > Verified WebSearch > Unve
- [ ] Publication dates checked (prefer recent/current)
- [ ] Confidence levels assigned honestly
- [ ] "What might I have missed?" review completed
- [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank)
</verification_protocol>
@@ -274,6 +275,20 @@ src/
**Key insight:** [why custom solutions are worse in this domain]
## Runtime State Inventory
> Include this section for rename/refactor/migration phases only. Omit entirely for greenfield phases.
| Category | Items Found | Action Required |
|----------|-------------|------------------|
| Stored data | [e.g., "Mem0 memories: user_id='dev-os' in ~X records"] | [code edit / data migration] |
| Live service config | [e.g., "25 n8n workflows in SQLite not exported to git"] | [API patch / manual] |
| OS-registered state | [e.g., "Windows Task Scheduler: 3 tasks with 'dev-os' in description"] | [re-register tasks] |
| Secrets/env vars | [e.g., "SOPS key 'webhook_auth_header' — code rename only, key unchanged"] | [none / update key] |
| Build artifacts | [e.g., "scripts/devos-cli/devos_cli.egg-info/ — stale after pyproject.toml rename"] | [reinstall package] |
**Nothing found in category:** State explicitly ("None — verified by X").
## Common Pitfalls
### Pitfall 1: [Name]
@@ -407,6 +422,26 @@ Based on phase description, identify what needs investigating:
- **Pitfalls:** Common beginner mistakes, gotchas, rewrite-causing errors
- **Don't Hand-Roll:** Existing solutions for deceptively complex problems
## Step 2.5: Runtime State Inventory (rename / refactor / migration phases only)
**Trigger:** Any phase involving rename, rebrand, refactor, string replacement, or migration.
A grep audit finds files. It does NOT find runtime state. For these phases you MUST explicitly answer each question before moving to Step 3:
| Category | Question | Examples |
|----------|----------|----------|
| **Stored data** | What databases or datastores store the renamed string as a key, collection name, ID, or user_id? | ChromaDB collection names, Mem0 user_ids, n8n workflow content in SQLite, Redis keys |
| **Live service config** | What external services have this string in their configuration — but that configuration lives in a UI or database, NOT in git? | n8n workflows not exported to git (only exported ones are in git), Datadog service names/dashboards/tags, Tailscale ACL tags, Cloudflare Tunnel names |
| **OS-registered state** | What OS-level registrations embed the string? | Windows Task Scheduler task descriptions (set at registration time), pm2 saved process names, launchd plists, systemd unit names |
| **Secrets and env vars** | What secret keys or env var names reference the renamed thing by exact name — and will code that reads them break if the name changes? | SOPS key names, .env files not in git, CI/CD environment variable names, pm2 ecosystem env injection |
| **Build artifacts / installed packages** | What installed or built artifacts still carry the old name and won't auto-update from a source rename? | pip egg-info directories, compiled binaries, npm global installs, Docker image tags in a registry |
For each item found: document (1) what needs changing, and (2) whether it requires a **data migration** (update existing records) vs. a **code edit** (change how new records are written). These are different tasks and must both appear in the plan.
**The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?*
If the answer for a category is "nothing" — say so explicitly. Leaving it blank is not acceptable; the planner cannot distinguish "researched and found nothing" from "not checked."
## Step 3: Execute Research Protocol
For each domain: Context7 first → Official docs → WebSearch → Cross-verify. Document findings with confidence levels as you go.
@@ -460,7 +495,7 @@ List missing test files, framework config, or shared fixtures needed before impl
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
|----|-------------|------------------|
| {REQ-ID} | {from REQUIREMENTS.md} | {which research findings enable implementation} |
</phase_requirements>
```
@@ -556,4 +591,4 @@ Quality indicators:
- **Actionable:** Planner could create tasks based on this research
- **Current:** Year included in searches, publication dates checked
</success_criteria>
</success_criteria>

View File

@@ -64,6 +64,7 @@ const hasGemini = args.includes('--gemini');
const hasCodex = args.includes('--codex');
const hasCopilot = args.includes('--copilot');
const hasAntigravity = args.includes('--antigravity');
const hasCursor = args.includes('--cursor');
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
const hasAll = args.includes('--all');
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
@@ -71,7 +72,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
// Runtime selection - can be set by flags or interactive prompt
let selectedRuntimes = [];
if (hasAll) {
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity'];
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor'];
} else if (hasBoth) {
selectedRuntimes = ['claude', 'opencode'];
} else {
@@ -81,6 +82,7 @@ if (hasAll) {
if (hasCodex) selectedRuntimes.push('codex');
if (hasCopilot) selectedRuntimes.push('copilot');
if (hasAntigravity) selectedRuntimes.push('antigravity');
if (hasCursor) selectedRuntimes.push('cursor');
}
// WSL + Windows Node.js detection
@@ -124,6 +126,7 @@ function getDirName(runtime) {
if (runtime === 'gemini') return '.gemini';
if (runtime === 'codex') return '.codex';
if (runtime === 'antigravity') return '.agent';
if (runtime === 'cursor') return '.cursor';
return '.claude';
}
@@ -151,6 +154,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
if (!isGlobal) return "'.agent'";
return "'.gemini', 'antigravity'";
}
if (runtime === 'cursor') return "'.cursor'";
return "'.claude'";
}
@@ -237,6 +241,18 @@ function getGlobalDir(runtime, explicitDir = null) {
return path.join(os.homedir(), '.gemini', 'antigravity');
}
if (runtime === 'cursor') {
// Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor
if (explicitDir) {
return expandTilde(explicitDir);
}
if (process.env.CURSOR_CONFIG_DIR) {
return expandTilde(process.env.CURSOR_CONFIG_DIR);
}
return path.join(os.homedir(), '.cursor');
}
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
if (explicitDir) {
return expandTilde(explicitDir);
@@ -257,7 +273,7 @@ const banner = '\n' +
'\n' +
' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
' A meta-prompting, context engineering and spec-driven\n' +
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, and Antigravity by TÂCHES.\n';
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, and Cursor by TÂCHES.\n';
// Parse --config-dir argument
function parseConfigDirArg() {
@@ -295,7 +311,7 @@ if (hasUninstall) {
// Show help if requested
if (hasHelp) {
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`);
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`);
process.exit(0);
}
@@ -551,6 +567,8 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
c = c.replace(/\.claude\//g, '.github/');
// CONV-07: Command name conversion (all gsd: references → gsd-)
c = c.replace(/gsd:/g, 'gsd-');
// Runtime-neutral agent name replacement (#766)
c = neutralizeAgentReferences(c, 'copilot-instructions.md');
return c;
}
@@ -641,6 +659,8 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) {
c = c.replace(/\.claude\//g, '.agent/');
// Command name conversion (all gsd: references → gsd-)
c = c.replace(/gsd:/g, 'gsd-');
// Runtime-neutral agent name replacement (#766)
c = neutralizeAgentReferences(c, 'GEMINI.md');
return c;
}
@@ -694,6 +714,14 @@ function yamlQuote(value) {
return JSON.stringify(value);
}
function yamlIdentifier(value) {
const text = String(value).trim();
if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) {
return text;
}
return yamlQuote(text);
}
function extractFrontmatterAndBody(content) {
if (!content.startsWith('---')) {
return { frontmatter: null, body: content };
@@ -717,6 +745,121 @@ function extractFrontmatterField(frontmatter, fieldName) {
return match[1].trim().replace(/^['"]|['"]$/g, '');
}
// Tool name mapping from Claude Code to Cursor CLI
const claudeToCursorTools = {
Bash: 'Shell',
Edit: 'StrReplace',
AskUserQuestion: null, // No direct equivalent — use conversational prompting
SlashCommand: null, // No equivalent — skills are auto-discovered
};
/**
* Convert a Claude Code tool name to Cursor CLI format
* @returns {string|null} Cursor tool name, or null if tool should be excluded
*/
function convertCursorToolName(claudeTool) {
if (claudeTool in claudeToCursorTools) {
return claudeToCursorTools[claudeTool];
}
// MCP tools keep their format (Cursor supports MCP)
if (claudeTool.startsWith('mcp__')) {
return claudeTool;
}
// Most tools share the same name (Read, Write, Glob, Grep, Task, WebSearch, WebFetch, TodoWrite)
return claudeTool;
}
function convertSlashCommandsToCursorSkillMentions(content) {
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
// This preserves rendered "next step" commands like "/gsd-execute-phase 17".
return content.replace(/gsd:/gi, 'gsd-');
}
function convertClaudeToCursorMarkdown(content) {
let converted = convertSlashCommandsToCursorSkillMentions(content);
// Replace tool name references in body text
converted = converted.replace(/\bBash\(/g, 'Shell(');
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
// Replace subagent_type from Claude to Cursor format
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Replace project-level Claude conventions with Cursor equivalents
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/');
converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/');
converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/');
// Remove Claude Code-specific bug workarounds before brand replacement
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
// Replace "Claude Code" brand references with "Cursor"
converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
return converted;
}
function getCursorSkillAdapterHeader(skillName) {
return `<cursor_skill_adapter>
## A. Skill Invocation
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
## B. User Prompting
When the workflow needs user input, prompt the user conversationally:
- Present options as a numbered list in your response text
- Ask the user to reply with their choice
- For multi-select, ask for comma-separated numbers
## C. Tool Usage
Use these Cursor tools when executing GSD workflows:
- \`Shell\` for running commands (terminal operations)
- \`StrReplace\` for editing existing files
- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
## D. Subagent Spawning
When the workflow needs to spawn a subagent:
- Use \`Task(subagent_type="generalPurpose", ...)\`
- The \`model\` parameter maps to Cursor's model options (e.g., "fast")
</cursor_skill_adapter>`;
}
function convertClaudeCommandToCursorSkill(content, skillName) {
const converted = convertClaudeToCursorMarkdown(content);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
let description = `Run GSD workflow ${skillName}.`;
if (frontmatter) {
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
if (maybeDescription) {
description = maybeDescription;
}
}
description = toSingleLine(description);
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
const adapter = getCursorSkillAdapterHeader(skillName);
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
}
/**
* Convert Claude Code agent markdown to Cursor agent format.
* Strips frontmatter fields Cursor doesn't support (color, skills),
* converts tool references, and adds a role context header.
*/
function convertClaudeAgentToCursorAgent(content) {
let converted = convertClaudeToCursorMarkdown(content);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
if (!frontmatter) return converted;
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
const description = extractFrontmatterField(frontmatter, 'description') || '';
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
return `${cleanFrontmatter}\n${body}`;
}
function convertSlashCommandsToCodexSkillMentions(content) {
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
return `$gsd-${String(commandName).toLowerCase()}`;
@@ -728,6 +871,8 @@ function convertSlashCommandsToCodexSkillMentions(content) {
function convertClaudeToCodexMarkdown(content) {
let converted = convertSlashCommandsToCodexSkillMentions(content);
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Runtime-neutral agent name replacement (#766)
converted = neutralizeAgentReferences(converted, 'AGENTS.md');
return converted;
}
@@ -819,14 +964,22 @@ purpose: ${toSingleLine(description)}
/**
* Generate a per-agent .toml config file for Codex.
* Sets sandbox_mode and developer_instructions from the agent markdown body.
* Sets required agent metadata, sandbox_mode, and developer_instructions
* from the agent markdown content.
*/
function generateCodexAgentToml(agentName, agentContent) {
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
const { body } = extractFrontmatterAndBody(agentContent);
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
const frontmatterText = frontmatter || '';
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
const resolvedDescription = toSingleLine(
extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}`
);
const instructions = body.trim();
const lines = [
`name = ${JSON.stringify(resolvedName)}`,
`description = ${JSON.stringify(resolvedDescription)}`,
`sandbox_mode = "${sandboxMode}"`,
// Agent prompts contain raw backslashes in regexes and shell snippets.
// TOML literal multiline strings preserve them without escape parsing.
@@ -857,6 +1010,10 @@ function generateCodexConfigBlock(agents) {
return lines.join('\n');
}
function stripCodexGsdAgentSections(content) {
return content.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
}
/**
* Strip GSD sections from Codex config.toml content.
* Returns cleaned content, or null if file would be empty.
@@ -882,7 +1039,7 @@ function stripGsdFromCodexConfig(content) {
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
// Remove [agents.gsd-*] sections (from header to next section or EOF)
cleaned = cleaned.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
cleaned = stripCodexGsdAgentSections(cleaned);
// Remove [features] section if now empty (only header, no keys before next section)
cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
@@ -916,7 +1073,7 @@ function mergeCodexConfig(configPath, gsdBlock) {
let before = existing.substring(0, markerIndex).trimEnd();
if (before) {
// Strip any GSD-managed sections that leaked above the marker from previous installs
before = before.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
before = stripCodexGsdAgentSections(before);
before = before.replace(/^\[agents\]\n(?:(?!\[)[^\n]*\n?)*/m, '');
before = before.replace(/\n{3,}/g, '\n\n').trimEnd();
@@ -929,7 +1086,14 @@ function mergeCodexConfig(configPath, gsdBlock) {
// Case 3: No marker — append GSD block
let content = existing;
content = content.trimEnd() + '\n\n' + gsdBlock + '\n';
content = stripCodexGsdAgentSections(content);
content = content.replace(/\n{3,}/g, '\n\n').trimEnd();
if (content) {
content = content + '\n\n' + gsdBlock + '\n';
} else {
content = gsdBlock + '\n';
}
fs.writeFileSync(configPath, content);
}
@@ -1036,6 +1200,36 @@ function installCodexConfig(targetDir, agentsSrc) {
* Terminals don't support subscript — Gemini renders these as raw HTML.
* Converts <sub>text</sub> to italic *(text)* for readable terminal output.
*/
/**
* Runtime-neutral agent name and instruction file replacement.
* Used by ALL non-Claude runtime converters to avoid Claude-specific
* references in workflow prompts, agent definitions, and documentation.
*
* Replaces:
* - Standalone "Claude" (agent name) → "the agent"
* Preserves: "Claude Code" (product), "Claude Opus/Sonnet/Haiku" (models),
* "claude-" (prefixes), "CLAUDE.md" (handled separately)
* - "CLAUDE.md" → runtime-appropriate instruction file
* - "Do NOT load full AGENTS.md" → removed (harmful for AGENTS.md runtimes)
*
* @param {string} content - File content to neutralize
* @param {string} instructionFile - Runtime's instruction file ('AGENTS.md', 'GEMINI.md', etc.)
* @returns {string} Content with runtime-neutral references
*/
function neutralizeAgentReferences(content, instructionFile) {
let c = content;
// Replace standalone "Claude" (the agent) but preserve product/model names.
// Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based
c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent');
// Replace CLAUDE.md with runtime-appropriate instruction file
if (instructionFile) {
c = c.replace(/CLAUDE\.md/g, instructionFile);
}
// Remove instructions that conflict with AGENTS.md-based runtimes
c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, '');
return c;
}
function stripSubTags(content) {
return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
}
@@ -1140,7 +1334,9 @@ function convertClaudeToGeminiAgent(content) {
// is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`;
// Runtime-neutral agent name replacement (#766)
const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
}
function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
@@ -1156,6 +1352,8 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
// Replace general-purpose subagent type with OpenCode's equivalent "general"
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
// Runtime-neutral agent name replacement (#766)
convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
// Check if content has frontmatter
if (!convertedContent.startsWith('---')) {
@@ -1228,6 +1426,13 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
continue;
}
// Strip model: field — OpenCode doesn't support Claude Code model aliases
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
// its configured default model. See #1156.
if (trimmed.startsWith('model:')) {
continue;
}
// Convert color names to hex for opencode (commands only; agents strip color above)
if (trimmed.startsWith('color:')) {
const colorValue = trimmed.substring(6).trim().toLowerCase();
@@ -1264,8 +1469,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
}
// For agents: add required OpenCode agent fields
// Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
// keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
// use its default model for subagents. See #1156.
if (isAgent) {
newLines.push('model: inherit');
newLines.push('mode: subagent');
}
@@ -1445,6 +1652,59 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
recurse(srcDir, prefix);
}
function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
if (!fs.existsSync(srcDir)) {
return;
}
fs.mkdirSync(skillsDir, { recursive: true });
// Remove previous GSD Cursor skills to avoid stale command skills
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
for (const entry of existing) {
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
}
}
function recurse(currentSrcDir, currentPrefix) {
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(currentSrcDir, entry.name);
if (entry.isDirectory()) {
recurse(srcPath, `${currentPrefix}-${entry.name}`);
continue;
}
if (!entry.name.endsWith('.md')) {
continue;
}
const baseName = entry.name.replace('.md', '');
const skillName = `${currentPrefix}-${baseName}`;
const skillDir = path.join(skillsDir, skillName);
fs.mkdirSync(skillDir, { recursive: true });
let content = fs.readFileSync(srcPath, 'utf8');
const globalClaudeRegex = /~\/\.claude\//g;
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
const localClaudeRegex = /\.\/\.claude\//g;
const cursorDirRegex = /~\/\.cursor\//g;
content = content.replace(globalClaudeRegex, pathPrefix);
content = content.replace(globalClaudeHomeRegex, pathPrefix);
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
content = content.replace(cursorDirRegex, pathPrefix);
content = processAttribution(content, getCommitAttribution(runtime));
content = convertClaudeCommandToCursorSkill(content, skillName);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
}
}
recurse(srcDir, prefix);
}
/**
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
@@ -1561,6 +1821,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
const isCursor = runtime === 'cursor';
const dirName = getDirName(runtime);
// Clean install: remove existing destination to prevent orphaned files
@@ -1617,6 +1878,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
content = convertClaudeToAntigravityContent(content, isGlobal);
content = processAttribution(content, getCommitAttribution(runtime));
fs.writeFileSync(destPath, content);
} else if (isCursor) {
content = convertClaudeToCursorMarkdown(content);
fs.writeFileSync(destPath, content);
} else {
fs.writeFileSync(destPath, content);
}
@@ -1630,6 +1894,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
let content = fs.readFileSync(srcPath, 'utf8');
content = convertClaudeToAntigravityContent(content, isGlobal);
fs.writeFileSync(destPath, content);
} else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
// For Cursor, also convert Claude references in JS/CJS utility scripts
let jsContent = fs.readFileSync(srcPath, 'utf8');
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/');
jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/');
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor');
fs.writeFileSync(destPath, jsContent);
} else {
fs.copyFileSync(srcPath, destPath);
}
@@ -1722,6 +1994,7 @@ function uninstall(isGlobal, runtime = 'claude') {
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
const isCursor = runtime === 'cursor';
const dirName = getDirName(runtime);
// Get the target directory based on runtime and install type
@@ -1739,6 +2012,7 @@ function uninstall(isGlobal, runtime = 'claude') {
if (runtime === 'codex') runtimeLabel = 'Codex';
if (runtime === 'copilot') runtimeLabel = 'Copilot';
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
if (runtime === 'cursor') runtimeLabel = 'Cursor';
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
@@ -1765,8 +2039,8 @@ function uninstall(isGlobal, runtime = 'claude') {
}
console.log(` ${green}✓${reset} Removed GSD commands from command/`);
}
} else if (isCodex) {
// Codex: remove skills/gsd-*/SKILL.md skill directories
} else if (isCodex || isCursor) {
// Codex/Cursor: remove skills/gsd-*/SKILL.md skill directories
const skillsDir = path.join(targetDir, 'skills');
if (fs.existsSync(skillsDir)) {
let skillCount = 0;
@@ -1779,11 +2053,12 @@ function uninstall(isGlobal, runtime = 'claude') {
}
if (skillCount > 0) {
removedCount++;
console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
console.log(` ${green}✓${reset} Removed ${skillCount} ${runtimeLabel} skills`);
}
}
// Codex: remove GSD agent .toml config files
// Codex-only: remove GSD agent .toml config files and config.toml sections
if (isCodex) {
const codexAgentsDir = path.join(targetDir, 'agents');
if (fs.existsSync(codexAgentsDir)) {
const tomlFiles = fs.readdirSync(codexAgentsDir);
@@ -1816,6 +2091,7 @@ function uninstall(isGlobal, runtime = 'claude') {
console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`);
}
}
}
} else if (isCopilot) {
// Copilot: remove skills/gsd-*/ directories (same layout as Codex skills)
const skillsDir = path.join(targetDir, 'skills');
@@ -1866,6 +2142,23 @@ function uninstall(isGlobal, runtime = 'claude') {
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
}
}
} else if (isCursor) {
// Cursor: remove skills/gsd-*/ directories (same layout as Codex skills)
const skillsDir = path.join(targetDir, 'skills');
if (fs.existsSync(skillsDir)) {
let skillCount = 0;
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
skillCount++;
}
}
if (skillCount > 0) {
removedCount++;
console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`);
}
}
} else {
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
if (fs.existsSync(gsdCommandsDir)) {
@@ -2274,6 +2567,7 @@ function writeManifest(configDir, runtime = 'claude') {
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
const isCursor = runtime === 'cursor';
const gsdDir = path.join(configDir, 'get-shit-done');
const commandsDir = path.join(configDir, 'commands', 'gsd');
const opencodeCommandDir = path.join(configDir, 'command');
@@ -2285,7 +2579,7 @@ function writeManifest(configDir, runtime = 'claude') {
for (const [rel, hash] of Object.entries(gsdHashes)) {
manifest.files['get-shit-done/' + rel] = hash;
}
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && fs.existsSync(commandsDir)) {
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && fs.existsSync(commandsDir)) {
const cmdHashes = generateManifest(commandsDir);
for (const [rel, hash] of Object.entries(cmdHashes)) {
manifest.files['commands/gsd/' + rel] = hash;
@@ -2298,7 +2592,7 @@ function writeManifest(configDir, runtime = 'claude') {
}
}
}
if ((isCodex || isCopilot || isAntigravity) && fs.existsSync(codexSkillsDir)) {
if ((isCodex || isCopilot || isAntigravity || isCursor) && fs.existsSync(codexSkillsDir)) {
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
const skillRoot = path.join(codexSkillsDir, skillName);
const skillHashes = generateManifest(skillRoot);
@@ -2314,6 +2608,18 @@ function writeManifest(configDir, runtime = 'claude') {
}
}
}
// Track hook files so saveLocalPatches() can detect user modifications
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot)
if (!isCodex && !isCopilot) {
const hooksDir = path.join(configDir, 'hooks');
if (fs.existsSync(hooksDir)) {
for (const file of fs.readdirSync(hooksDir)) {
if (file.startsWith('gsd-') && file.endsWith('.js')) {
manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
}
}
}
}
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
return manifest;
@@ -2376,7 +2682,9 @@ function reportLocalPatches(configDir, runtime = 'claude') {
? '/gsd-reapply-patches'
: runtime === 'codex'
? '$gsd-reapply-patches'
: '/gsd:reapply-patches';
: runtime === 'cursor'
? 'gsd-reapply-patches (mention the skill name)'
: '/gsd:reapply-patches';
console.log('');
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
for (const f of meta.files) {
@@ -2397,6 +2705,7 @@ function install(isGlobal, runtime = 'claude') {
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
const isCursor = runtime === 'cursor';
const dirName = getDirName(runtime);
const src = path.join(__dirname, '..');
@@ -2411,9 +2720,12 @@ function install(isGlobal, runtime = 'claude') {
// Path prefix for file references in markdown content (e.g. gsd-tools.cjs).
// Replaces $HOME/.claude/ or ~/.claude/ so the result is <pathPrefix>get-shit-done/bin/...
// Always use absolute path so: (1) local installs work when GSD is outside $HOME,
// (2) spawned subagents with empty $HOME still resolve the path (fixes #820).
const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`;
// For global installs: use ~/ so paths work across environments (e.g. Docker
// containers mounting ~/.claude from a Windows host where os.homedir() differs).
// For local installs: use resolved absolute path (may be outside $HOME).
const pathPrefix = isGlobal
? path.resolve(targetDir).replace(os.homedir(), '~').replace(/\\/g, '/') + '/'
: `${path.resolve(targetDir).replace(/\\/g, '/')}/`;
let runtimeLabel = 'Claude Code';
if (isOpencode) runtimeLabel = 'OpenCode';
@@ -2421,6 +2733,7 @@ function install(isGlobal, runtime = 'claude') {
if (isCodex) runtimeLabel = 'Codex';
if (isCopilot) runtimeLabel = 'Copilot';
if (isAntigravity) runtimeLabel = 'Antigravity';
if (isCursor) runtimeLabel = 'Cursor';
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
@@ -2488,6 +2801,16 @@ function install(isGlobal, runtime = 'claude') {
} else {
failures.push('skills/gsd-*');
}
} else if (isCursor) {
const skillsDir = path.join(targetDir, 'skills');
const gsdSrc = path.join(src, 'commands', 'gsd');
copyCommandsAsCursorSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
const installedSkillNames = listCodexSkillNames(skillsDir); // reuse — same dir structure
if (installedSkillNames.length > 0) {
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
} else {
failures.push('skills/gsd-*');
}
} else {
// Claude Code & Gemini: nested structure in commands/ directory
const commandsDir = path.join(targetDir, 'commands');
@@ -2552,6 +2875,8 @@ function install(isGlobal, runtime = 'claude') {
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
} else if (isAntigravity) {
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
} else if (isCursor) {
content = convertClaudeAgentToCursorAgent(content);
}
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
fs.writeFileSync(path.join(agentsDest, destName), content);
@@ -2585,7 +2910,7 @@ function install(isGlobal, runtime = 'claude') {
failures.push('VERSION');
}
if (!isCodex && !isCopilot) {
if (!isCodex && !isCopilot && !isCursor) {
// Write package.json to force CommonJS mode for GSD scripts
// Prevents "require is not defined" errors when project has "type": "module"
// Node.js walks up looking for package.json - this stops inheritance from project
@@ -2606,10 +2931,14 @@ function install(isGlobal, runtime = 'claude') {
if (fs.statSync(srcFile).isFile()) {
const destFile = path.join(hooksDest, entry);
// Template .js files to replace '.claude' with runtime-specific config dir
// and stamp the current GSD version into the hook version header
if (entry.endsWith('.js')) {
let content = fs.readFileSync(srcFile, 'utf8');
content = content.replace(/'\.claude'/g, configDirReplacement);
content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
fs.writeFileSync(destFile, content);
// Ensure hook files are executable (fixes #1162 — missing +x permission)
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
} else {
fs.copyFileSync(srcFile, destFile);
}
@@ -2689,6 +3018,60 @@ function install(isGlobal, runtime = 'claude') {
const agentCount = installCodexConfig(targetDir, agentsSrc);
console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`);
console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`);
// Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag
const configPath = path.join(targetDir, 'config.toml');
try {
let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
// Enable hooks feature flag if not present
if (!configContent.includes('codex_hooks')) {
if (configContent.includes('[features]')) {
// Insert codex_hooks = true right after the [features] header.
// Fixes #1202: previous approach could leave non-boolean keys (like
// model = "gpt-5.4") under [features], causing Codex TOML parse errors.
configContent = configContent.replace(/(\[features\]\n)/, '$1codex_hooks = true\n');
} else {
configContent = '[features]\ncodex_hooks = true\n\n' + configContent;
}
}
// Safety check: detect non-boolean keys under [features] that would break Codex (#1202).
// Extract the [features] section content (between [features] and next [section] or EOF).
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
if (featuresMatch) {
const featuresBody = featuresMatch[1];
const nonBooleanKeys = featuresBody.split('\n')
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
.map(line => line.trim());
if (nonBooleanKeys.length > 0) {
// Move non-boolean keys above [features] to prevent TOML parse errors
let cleanedFeatures = featuresBody.split('\n')
.filter(line => !line.match(/^\s*\w+\s*=/) || line.match(/=\s*(true|false)\s*(#.*)?$/))
.join('\n');
const movedKeys = nonBooleanKeys.join('\n') + '\n';
configContent = configContent.replace(
/\[features\]\n[\s\S]*?(?=\n\[|$)/,
movedKeys + '\n[features]\n' + cleanedFeatures.trim() + '\n'
);
console.log(` ${yellow}⚠${reset} Moved ${nonBooleanKeys.length} non-feature key(s) out of [features] section to prevent TOML errors`);
}
}
// Add SessionStart hook for update checking
const updateCheckScript = path.resolve(targetDir, 'get-shit-done', 'hooks', 'gsd-update-check.js').replace(/\\/g, '/');
const hookBlock = `\n# GSD Hooks\n[[hooks]]\nevent = "SessionStart"\ncommand = "node ${updateCheckScript}"\n`;
if (!configContent.includes('gsd-update-check')) {
configContent += hookBlock;
}
fs.writeFileSync(configPath, configContent, 'utf-8');
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart)`);
} catch (e) {
console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
}
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
}
@@ -2705,6 +3088,11 @@ function install(isGlobal, runtime = 'claude') {
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
}
if (isCursor) {
// Cursor uses skills — no config.toml, no settings.json hooks needed
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
}
// Configure statusline and hooks in settings.json
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
@@ -2788,8 +3176,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
const isOpencode = runtime === 'opencode';
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isCursor = runtime === 'cursor';
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot) {
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor) {
settings.statusLine = {
type: 'command',
command: statuslineCommand
@@ -2798,7 +3187,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
}
// Write settings when runtime supports settings.json
if (!isCodex && !isCopilot) {
if (!isCodex && !isCopilot && !isCursor) {
writeSettings(settingsPath, settings);
}
@@ -2813,12 +3202,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
if (runtime === 'codex') program = 'Codex';
if (runtime === 'copilot') program = 'Copilot';
if (runtime === 'antigravity') program = 'Antigravity';
if (runtime === 'cursor') program = 'Cursor';
let command = '/gsd:new-project';
if (runtime === 'opencode') command = '/gsd-new-project';
if (runtime === 'codex') command = '$gsd-new-project';
if (runtime === 'copilot') command = '/gsd-new-project';
if (runtime === 'antigravity') command = '/gsd-new-project';
if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)';
console.log(`
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
@@ -2902,15 +3293,18 @@ function promptRuntime(callback) {
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
${cyan}7${reset}) All
${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset}
${cyan}8${reset}) All
`);
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
answered = true;
rl.close();
const choice = answer.trim() || '1';
if (choice === '7') {
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']);
if (choice === '8') {
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']);
} else if (choice === '7') {
callback(['cursor']);
} else if (choice === '6') {
callback(['antigravity']);
} else if (choice === '5') {
@@ -3010,7 +3404,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
// Test-only exports — skip main logic when loaded as a module for testing
if (process.env.GSD_TEST_MODE) {
module.exports = {
yamlIdentifier,
getCodexSkillAdapterHeader,
convertClaudeCommandToCursorSkill,
convertClaudeAgentToCursorAgent,
convertClaudeToGeminiAgent,
convertClaudeAgentToCodexAgent,
generateCodexAgentToml,
@@ -3020,6 +3417,7 @@ if (process.env.GSD_TEST_MODE) {
installCodexConfig,
convertClaudeCommandToCodexSkill,
convertClaudeToOpencodeFrontmatter,
neutralizeAgentReferences,
GSD_CODEX_MARKER,
CODEX_AGENT_SANDBOX,
getDirName,

View File

@@ -0,0 +1,76 @@
---
name: gsd:add-backlog
description: Add an idea to the backlog parking lot (999.x numbering)
argument-hint: <description>
allowed-tools:
- Read
- Write
- Bash
---
<objective>
Add a backlog item to the roadmap using 999.x numbering. Backlog items are
unsequenced ideas that aren't ready for active planning — they live outside
the normal phase sequence and accumulate context over time.
</objective>
<process>
1. **Read ROADMAP.md** to find existing backlog entries:
```bash
cat .planning/ROADMAP.md
```
2. **Find next backlog number:**
```bash
NEXT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 999 --raw)
```
If no 999.x phases exist, start at 999.1.
3. **Create the phase directory:**
```bash
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
mkdir -p ".planning/phases/${NEXT}-${SLUG}"
touch ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
```
4. **Add to ROADMAP.md** under a `## Backlog` section. If the section doesn't exist, create it at the end:
```markdown
## Backlog
### Phase {NEXT}: {description} (BACKLOG)
**Goal:** [Captured for future planning]
**Requirements:** TBD
**Plans:** 0 plans
Plans:
- [ ] TBD (promote with /gsd:review-backlog when ready)
```
5. **Commit:**
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
```
6. **Report:**
```
## 📋 Backlog Item Added
Phase {NEXT}: {description}
Directory: .planning/phases/{NEXT}-{slug}/
This item lives in the backlog parking lot.
Use /gsd:discuss-phase {NEXT} to explore it further.
Use /gsd:review-backlog to promote items to active milestone.
```
</process>
<notes>
- 999.x numbering keeps backlog items out of the active phase sequence
- Phase directories are created immediately, so /gsd:discuss-phase and /gsd:plan-phase work on them
- No `Depends on:` field — backlog items are unsequenced by definition
- Sparse numbering is fine (999.1, 999.3) — always uses next-decimal
</notes>

24
commands/gsd/audit-uat.md Normal file
View File

@@ -0,0 +1,24 @@
---
name: gsd:audit-uat
description: Cross-phase audit of all outstanding UAT and verification items
allowed-tools:
- Read
- Glob
- Grep
- Bash
---
<objective>
Scan all phases for pending, skipped, blocked, and human_needed UAT items. Cross-reference against codebase to detect stale documentation. Produce prioritized human test plan.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/audit-uat.md
</execution_context>
<context>
Core planning files are loaded in-workflow via CLI.
**Scope:**
Glob: .planning/phases/*/*-UAT.md
Glob: .planning/phases/*/*-VERIFICATION.md
</context>

View File

@@ -1,7 +1,7 @@
---
name: gsd:discuss-phase
description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults).
argument-hint: "<phase> [--auto]"
argument-hint: "<phase> [--auto] [--batch] [--analyze]"
allowed-tools:
- Read
- Write

View File

@@ -1,7 +1,7 @@
---
name: gsd:execute-phase
description: Execute all plans in a phase with wave-based parallelization
argument-hint: "<phase-number> [--gaps-only]"
argument-hint: "<phase-number> [--gaps-only] [--interactive]"
allowed-tools:
- Read
- Write
@@ -31,6 +31,7 @@ Phase: $ARGUMENTS
**Flags:**
- `--gaps-only` — Execute only gap closure plans (plans with `gap_closure: true` in frontmatter). Use after verify-work creates fix plans.
- `--interactive` — Execute plans sequentially inline (no subagents) with user checkpoints between tasks. Lower token usage, pair-programming style. Best for small phases, bug fixes, and verification gaps.
Context files are resolved inside the workflow via `gsd-tools init execute-phase` and per-subagent `<files_to_read>` blocks.
</context>

30
commands/gsd/fast.md Normal file
View File

@@ -0,0 +1,30 @@
---
name: gsd:fast
description: Execute a trivial task inline — no subagents, no planning overhead
argument-hint: "[task description]"
allowed-tools:
- Read
- Write
- Edit
- Bash
- Grep
- Glob
---
<objective>
Execute a trivial task directly in the current context without spawning subagents
or generating PLAN.md files. For tasks too small to justify planning overhead:
typo fixes, config changes, small refactors, forgotten commits, simple additions.
This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that
needs research, multi-step planning, or verification. /gsd:fast is for tasks
you could describe in one sentence and execute in under 2 minutes.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/fast.md
</execution_context>
<process>
Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end.
</process>

24
commands/gsd/next.md Normal file
View File

@@ -0,0 +1,24 @@
---
name: gsd:next
description: Automatically advance to the next logical step in the GSD workflow
allowed-tools:
- Read
- Bash
- Grep
- Glob
- SlashCommand
---
<objective>
Detect the current project state and automatically invoke the next logical GSD workflow step.
No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next.
Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/next.md
</execution_context>
<process>
Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end.
</process>

View File

@@ -0,0 +1,28 @@
---
name: gsd:plant-seed
description: Capture a forward-looking idea with trigger conditions — surfaces automatically at the right milestone
argument-hint: "[idea summary]"
allowed-tools:
- Read
- Write
- Edit
- Bash
- AskUserQuestion
---
<objective>
Capture an idea that's too big for now but should surface automatically when the right
milestone arrives. Seeds solve context rot: instead of a one-liner in Deferred that nobody
reads, a seed preserves the full WHY, WHEN to surface, and breadcrumbs to details.
Creates: .planning/seeds/SEED-NNN-slug.md
Consumed by: /gsd:new-milestone (scans seeds and presents matches)
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/plant-seed.md
</execution_context>
<process>
Execute the plant-seed workflow from @~/.claude/get-shit-done/workflows/plant-seed.md end-to-end.
</process>

25
commands/gsd/pr-branch.md Normal file
View File

@@ -0,0 +1,25 @@
---
name: gsd:pr-branch
description: Create a clean PR branch by filtering out .planning/ commits — ready for code review
argument-hint: "[target branch, default: main]"
allowed-tools:
- Bash
- Read
- AskUserQuestion
---
<objective>
Create a clean branch suitable for pull requests by filtering out .planning/ commits
from the current branch. Reviewers see only code changes, not GSD planning artifacts.
This solves the problem of PR diffs being cluttered with PLAN.md, SUMMARY.md, STATE.md
changes that are irrelevant to code review.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/pr-branch.md
</execution_context>
<process>
Execute the pr-branch workflow from @~/.claude/get-shit-done/workflows/pr-branch.md end-to-end.
</process>

View File

@@ -0,0 +1,61 @@
---
name: gsd:review-backlog
description: Review and promote backlog items to active milestone
allowed-tools:
- Read
- Write
- Bash
---
<objective>
Review all 999.x backlog items and optionally promote them into the active
milestone sequence or remove stale entries.
</objective>
<process>
1. **List backlog items:**
```bash
ls -d .planning/phases/999* 2>/dev/null || echo "No backlog items found"
```
2. **Read ROADMAP.md** and extract all 999.x phase entries:
```bash
cat .planning/ROADMAP.md
```
Show each backlog item with its description, any accumulated context (CONTEXT.md, RESEARCH.md), and creation date.
3. **Present the list to the user** via AskUserQuestion:
- For each backlog item, show: phase number, description, accumulated artifacts
- Options per item: **Promote** (move to active), **Keep** (leave in backlog), **Remove** (delete)
4. **For items to PROMOTE:**
- Find the next sequential phase number in the active milestone
- Rename the directory from `999.x-slug` to `{new_num}-slug`:
```bash
NEW_NUM=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase add "${DESCRIPTION}" --raw)
```
- Move accumulated artifacts to the new phase directory
- Update ROADMAP.md: move the entry from `## Backlog` section to the active phase list
- Remove `(BACKLOG)` marker
- Add appropriate `**Depends on:**` field
5. **For items to REMOVE:**
- Delete the phase directory
- Remove the entry from ROADMAP.md `## Backlog` section
6. **Commit changes:**
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: review backlog — promoted N, removed M" --files .planning/ROADMAP.md
```
7. **Report summary:**
```
## 📋 Backlog Review Complete
Promoted: {list of promoted items with new phase numbers}
Kept: {list of items remaining in backlog}
Removed: {list of deleted items}
```
</process>

37
commands/gsd/review.md Normal file
View File

@@ -0,0 +1,37 @@
---
name: gsd:review
description: Request cross-AI peer review of phase plans from external AI CLIs
argument-hint: "--phase N [--gemini] [--claude] [--codex] [--all]"
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
---
<objective>
Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans.
Produces a structured REVIEWS.md with per-reviewer feedback that can be fed back into
planning via /gsd:plan-phase --reviews.
**Flow:** Detect CLIs → Build review prompt → Invoke each CLI → Collect responses → Write REVIEWS.md
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/review.md
</execution_context>
<context>
Phase number: extracted from $ARGUMENTS (required)
**Flags:**
- `--gemini` — Include Gemini CLI review
- `--claude` — Include Claude CLI review (uses separate session)
- `--codex` — Include Codex CLI review
- `--all` — Include all available CLIs
</context>
<process>
Execute the review workflow from @~/.claude/get-shit-done/workflows/review.md end-to-end.
</process>

View File

@@ -0,0 +1,19 @@
---
name: gsd:session-report
description: Generate a session report with token usage estimates, work summary, and outcomes
allowed-tools:
- Read
- Bash
- Write
---
<objective>
Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/session-report.md
</execution_context>
<process>
Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end.
</process>

23
commands/gsd/ship.md Normal file
View File

@@ -0,0 +1,23 @@
---
name: gsd:ship
description: Create PR, run review, and prepare for merge after verification passes
argument-hint: "[phase number or milestone, e.g., '4' or 'v1.0']"
allowed-tools:
- Read
- Bash
- Grep
- Glob
- Write
- AskUserQuestion
---
<objective>
Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge.
Closes the plan → execute → verify → ship loop.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/ship.md
</execution_context>
Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end.

127
commands/gsd/thread.md Normal file
View File

@@ -0,0 +1,127 @@
---
name: gsd:thread
description: Manage persistent context threads for cross-session work
argument-hint: [name | description]
allowed-tools:
- Read
- Write
- Bash
---
<objective>
Create, list, or resume persistent context threads. Threads are lightweight
cross-session knowledge stores for work that spans multiple sessions but
doesn't belong to any specific phase.
</objective>
<process>
**Parse $ARGUMENTS to determine mode:**
<mode_list>
**If no arguments or $ARGUMENTS is empty:**
List all threads:
```bash
ls .planning/threads/*.md 2>/dev/null
```
For each thread, read the first few lines to show title and status:
```
## Active Threads
| Thread | Status | Last Updated |
|--------|--------|-------------|
| fix-deploy-key-auth | OPEN | 2026-03-15 |
| pasta-tcp-timeout | RESOLVED | 2026-03-12 |
| perf-investigation | IN PROGRESS | 2026-03-17 |
```
If no threads exist, show:
```
No threads found. Create one with: /gsd:thread <description>
```
</mode_list>
<mode_resume>
**If $ARGUMENTS matches an existing thread name (file exists):**
Resume the thread — load its context into the current session:
```bash
cat ".planning/threads/${THREAD_NAME}.md"
```
Display the thread content and ask what the user wants to work on next.
Update the thread's status to `IN PROGRESS` if it was `OPEN`.
</mode_resume>
<mode_create>
**If $ARGUMENTS is a new description (no matching thread file):**
Create a new thread:
1. Generate slug from description:
```bash
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
```
2. Create the threads directory if needed:
```bash
mkdir -p .planning/threads
```
3. Write the thread file:
```bash
cat > ".planning/threads/${SLUG}.md" << 'EOF'
# Thread: {description}
## Status: OPEN
## Goal
{description}
## Context
*Created from conversation on {today's date}.*
## References
- *(add links, file paths, or issue numbers)*
## Next Steps
- *(what the next session should do first)*
EOF
```
4. If there's relevant context in the current conversation (code snippets,
error messages, investigation results), extract and add it to the Context
section.
5. Commit:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create thread — ${ARGUMENTS}" --files ".planning/threads/${SLUG}.md"
```
6. Report:
```
## 🧵 Thread Created
Thread: {slug}
File: .planning/threads/{slug}.md
Resume anytime with: /gsd:thread {slug}
```
</mode_create>
</process>
<notes>
- Threads are NOT phase-scoped — they exist independently of the roadmap
- Lighter weight than /gsd:pause-work — no phase state, no plan context
- The value is in Context and Next Steps — a cold-start session can pick up immediately
- Threads can be promoted to phases or backlog items when they mature:
/gsd:add-phase or /gsd:add-backlog with context from the thread
- Thread files live in .planning/threads/ — no collision with phases or other GSD structures
</notes>

View File

@@ -171,7 +171,7 @@ Runtime hooks that integrate with the host AI agent:
### CLI Tools (`get-shit-done/bin/`)
Node.js CLI utility (`gsd-tools.cjs`) with 11 domain modules:
Node.js CLI utility (`gsd-tools.cjs`) with 15 domain modules:
| Module | Responsibility |
|--------|---------------|
@@ -247,6 +247,14 @@ Each executor gets:
- Project context (PROJECT.md, STATE.md)
- Phase context (CONTEXT.md, RESEARCH.md if available)
#### Parallel Commit Safety
When multiple executors run within the same wave, two mechanisms prevent conflicts:
1. **`--no-verify` commits** — Parallel agents skip pre-commit hooks (which can cause build lock contention, e.g., cargo lock fights in Rust projects). The orchestrator runs `git hook run pre-commit` once after each wave completes.
2. **STATE.md file locking** — All `writeStateMd()` calls use lockfile-based mutual exclusion (`STATE.md.lock` with `O_EXCL` atomic creation). This prevents the read-modify-write race condition where two agents read STATE.md, modify different fields, and the last writer overwrites the other's changes. Includes stale lock detection (10s timeout) and spin-wait with jitter.
---
## Data Flow
@@ -334,8 +342,8 @@ UI-SPEC.md (per phase) ───────────────────
├── commands/gsd/*.md # 37 slash commands
├── get-shit-done/
│ ├── bin/gsd-tools.cjs # CLI utility
│ ├── bin/lib/*.cjs # 11 domain modules
│ ├── workflows/*.md # 41 workflow definitions
│ ├── bin/lib/*.cjs # 15 domain modules
│ ├── workflows/*.md # 42 workflow definitions
│ ├── references/*.md # 13 shared reference docs
│ └── templates/ # Planning artifact templates
├── agents/*.md # 15 agent definitions
@@ -358,7 +366,7 @@ Equivalent paths for other runtimes:
```
.planning/
├── PROJECT.md # Project vision, constraints, decisions
├── PROJECT.md # Project vision, constraints, decisions, evolution rules
├── REQUIREMENTS.md # Scoped requirements (v1/v2/out-of-scope)
├── ROADMAP.md # Phase breakdown with status tracking
├── STATE.md # Living memory: position, decisions, blockers, metrics

View File

@@ -9,7 +9,7 @@
`gsd-tools.cjs` is a Node.js CLI utility that replaces repetitive inline bash patterns across GSD's ~50 command, workflow, and agent files. It centralizes: config parsing, model resolution, phase lookup, git commits, summary verification, state management, and template operations.
**Location:** `get-shit-done/bin/gsd-tools.cjs`
**Modules:** 11 domain modules in `get-shit-done/bin/lib/`
**Modules:** 15 domain modules in `get-shit-done/bin/lib/`
**Usage:**
```bash
@@ -330,8 +330,14 @@ node gsd-tools.cjs progress [json|table|bar]
# Complete a todo
node gsd-tools.cjs todo complete <filename>
# UAT audit — scan all phases for unresolved items
node gsd-tools.cjs audit-uat
# Git commit with config checks
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend]
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend] [--no-verify]
```
> **`--no-verify`**: Skips pre-commit hooks. Used by parallel executor agents during wave-based execution to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator runs hooks once after each wave completes. Do not use `--no-verify` during sequential execution — let hooks run normally.
# Web search (requires Brave API key)
node gsd-tools.cjs websearch <query> [--limit N] [--freshness day|week|month]
@@ -355,3 +361,6 @@ node gsd-tools.cjs websearch <query> [--limit N] [--freshness day|week|month]
| Milestone | `lib/milestone.cjs` | Milestone archival, requirements marking |
| Commands | `lib/commands.cjs` | Misc: slug, timestamp, todos, scaffold, stats, websearch |
| Model Profiles | `lib/model-profiles.cjs` | Profile resolution table |
| UAT | `lib/uat.cjs` | Cross-phase UAT/verification audit |
| Profile Output | `lib/profile-output.cjs` | Developer profile formatting |
| Profile Pipeline | `lib/profile-pipeline.cjs` | Session analysis pipeline |

View File

@@ -23,7 +23,7 @@ Initialize a new project with deep context gathering.
| `--auto @file.md` | Auto-extract from document, skip interactive questions |
**Prerequisites:** No existing `.planning/PROJECT.md`
**Produces:** `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`, `config.json`, `research/`
**Produces:** `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`, `config.json`, `research/`, `CLAUDE.md`
```bash
/gsd:new-project # Interactive mode
@@ -132,6 +132,71 @@ User acceptance testing with auto-diagnosis.
---
### `/gsd:next`
Automatically advance to the next logical workflow step. Reads project state and runs the appropriate command.
**Prerequisites:** `.planning/` directory exists
**Behavior:**
- No project → suggests `/gsd:new-project`
- Phase needs discussion → runs `/gsd:discuss-phase`
- Phase needs planning → runs `/gsd:plan-phase`
- Phase needs execution → runs `/gsd:execute-phase`
- Phase needs verification → runs `/gsd:verify-work`
- All phases complete → suggests `/gsd:complete-milestone`
```bash
/gsd:next # Auto-detect and run next step
```
---
### `/gsd:session-report`
Generate a session report with work summary, outcomes, and estimated resource usage.
**Prerequisites:** Active project with recent work
**Produces:** `.planning/reports/SESSION_REPORT.md`
```bash
/gsd:session-report # Generate post-session summary
```
**Report includes:**
- Work performed (commits, plans executed, phases progressed)
- Outcomes and deliverables
- Blockers and decisions made
- Estimated token/cost usage
- Next steps recommendation
---
### `/gsd:ship`
Create PR from completed phase work with auto-generated body.
| Argument | Required | Description |
|----------|----------|-------------|
| `N` | No | Phase number or milestone version (e.g., `4` or `v1.0`) |
| `--draft` | No | Create as draft PR |
**Prerequisites:** Phase verified (`/gsd:verify-work` passed), `gh` CLI installed and authenticated
**Produces:** GitHub PR with rich body from planning artifacts, STATE.md updated
```bash
/gsd:ship 4 # Ship phase 4
/gsd:ship 4 --draft # Ship as draft PR
```
**PR body includes:**
- Phase goal from ROADMAP.md
- Changes summary from SUMMARY.md files
- Requirements addressed (REQ-IDs)
- Verification status
- Key decisions
---
### `/gsd:ui-review`
Retroactive 6-pillar visual audit of implemented frontend.
@@ -150,6 +215,19 @@ Retroactive 6-pillar visual audit of implemented frontend.
---
### `/gsd:audit-uat`
Cross-phase audit of all outstanding UAT and verification items.
**Prerequisites:** At least one phase has been executed with UAT or verification
**Produces:** Categorized audit report with human test plan
```bash
/gsd:audit-uat
```
---
### `/gsd:audit-milestone`
Verify milestone met its definition of done.
@@ -183,6 +261,7 @@ Start next version cycle.
| Argument | Required | Description |
|----------|----------|-------------|
| `name` | No | Milestone name |
| `--reset-phase-numbers` | No | Restart the new milestone at Phase 1 and archive old phase dirs before roadmapping |
**Prerequisites:** Previous milestone completed
**Produces:** Updated `PROJECT.md`, new `REQUIREMENTS.md`, new `ROADMAP.md`
@@ -190,6 +269,7 @@ Start next version cycle.
```bash
/gsd:new-milestone # Interactive
/gsd:new-milestone "v2.0 Mobile" # Named milestone
/gsd:new-milestone --reset-phase-numbers "v2.0 Mobile" # Restart milestone numbering at 1
```
---

View File

@@ -42,7 +42,8 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}"
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
},
"gates": {
"confirm_project": true,
@@ -133,6 +134,8 @@ To keep planning artifacts out of git:
| `parallelization.max_concurrent_agents` | number | `3` | Maximum simultaneous agents |
| `parallelization.min_plans_for_parallel` | number | `2` | Minimum plans to trigger parallel execution |
> **Pre-commit hooks and parallel execution**: When parallelization is enabled, executor agents commit with `--no-verify` to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator validates hooks once after each wave completes. STATE.md writes are protected by file-level locking to prevent concurrent write corruption. If you need hooks to run per-commit, set `parallelization.enabled: false`.
---
## Git Branching
@@ -142,6 +145,7 @@ To keep planning artifacts out of git:
| `git.branching_strategy` | enum | `none` | `none`, `phase`, or `milestone` |
| `git.phase_branch_template` | string | `gsd/phase-{phase}-{slug}` | Branch name template for phase strategy |
| `git.milestone_branch_template` | string | `gsd/{milestone}-{slug}` | Branch name template for milestone strategy |
| `git.quick_branch_template` | string or null | `null` | Optional branch name template for `/gsd:quick` tasks |
### Strategy Comparison
@@ -158,6 +162,15 @@ To keep planning artifacts out of git:
| `{phase}` | `phase_branch_template` | `03` (zero-padded) |
| `{slug}` | Both templates | `user-authentication` (lowercase, hyphenated) |
| `{milestone}` | `milestone_branch_template` | `v1.0` |
| `{num}` / `{quick}` | `quick_branch_template` | `260317-abc` (quick task ID) |
Example quick-task branching:
```json
"git": {
"quick_branch_template": "gsd/quick-{num}-{slug}"
}
```
### Merge Options at Milestone Completion
@@ -246,7 +259,7 @@ Valid override values: `opus`, `sonnet`, `haiku`, `inherit`
| `quality` | Opus for all decision-making, Sonnet for verification | Quota available, critical architecture work |
| `balanced` | Opus for planning only, Sonnet for everything else | Normal development (default) |
| `budget` | Sonnet for code-writing, Haiku for research/verification | High-volume work, less critical phases |
| `inherit` | All agents use current session model | Dynamic model switching (OpenCode `/model`) |
| `inherit` | All agents use current session model | Dynamic model switching, **non-Anthropic providers** (OpenRouter, local models) |
---

View File

@@ -20,33 +20,39 @@
- [Quick Mode](#10-quick-mode)
- [Autonomous Mode](#11-autonomous-mode)
- [Freeform Routing](#12-freeform-routing)
- [Note Capture](#13-note-capture)
- [Auto-Advance (Next)](#14-auto-advance-next)
- [Quality Assurance Features](#quality-assurance-features)
- [Nyquist Validation](#13-nyquist-validation)
- [Plan Checking](#14-plan-checking)
- [Post-Execution Verification](#15-post-execution-verification)
- [Node Repair](#16-node-repair)
- [Health Validation](#17-health-validation)
- [Nyquist Validation](#15-nyquist-validation)
- [Plan Checking](#16-plan-checking)
- [Post-Execution Verification](#17-post-execution-verification)
- [Node Repair](#18-node-repair)
- [Health Validation](#19-health-validation)
- [Cross-Phase Regression Gate](#20-cross-phase-regression-gate)
- [Requirements Coverage Gate](#21-requirements-coverage-gate)
- [Context Engineering Features](#context-engineering-features)
- [Context Window Monitoring](#18-context-window-monitoring)
- [Session Management](#19-session-management)
- [Multi-Agent Orchestration](#20-multi-agent-orchestration)
- [Model Profiles](#21-model-profiles)
- [Context Window Monitoring](#22-context-window-monitoring)
- [Session Management](#23-session-management)
- [Session Reporting](#24-session-reporting)
- [Multi-Agent Orchestration](#25-multi-agent-orchestration)
- [Model Profiles](#26-model-profiles)
- [Brownfield Features](#brownfield-features)
- [Codebase Mapping](#22-codebase-mapping)
- [Codebase Mapping](#27-codebase-mapping)
- [Utility Features](#utility-features)
- [Debug System](#23-debug-system)
- [Todo Management](#24-todo-management)
- [Statistics Dashboard](#25-statistics-dashboard)
- [Update System](#26-update-system)
- [Settings Management](#27-settings-management)
- [Test Generation](#28-test-generation)
- [Debug System](#28-debug-system)
- [Todo Management](#29-todo-management)
- [Statistics Dashboard](#30-statistics-dashboard)
- [Update System](#31-update-system)
- [Settings Management](#32-settings-management)
- [Test Generation](#33-test-generation)
- [Infrastructure Features](#infrastructure-features)
- [Git Integration](#29-git-integration)
- [CLI Tools](#30-cli-tools)
- [Multi-Runtime Support](#31-multi-runtime-support)
- [Hook System](#32-hook-system)
- [Developer Profiling](#33-developer-profiling)
- [Execution Hardening](#34-execution-hardening)
- [Git Integration](#34-git-integration)
- [CLI Tools](#35-cli-tools)
- [Multi-Runtime Support](#36-multi-runtime-support)
- [Hook System](#37-hook-system)
- [Developer Profiling](#38-developer-profiling)
- [Execution Hardening](#39-execution-hardening)
- [Verification Debt Tracking](#40-verification-debt-tracking)
---
@@ -70,7 +76,7 @@
**Produces:**
| Artifact | Description |
|----------|-------------|
| `PROJECT.md` | Project vision, constraints, technical decisions |
| `PROJECT.md` | Project vision, constraints, technical decisions, evolution rules |
| `REQUIREMENTS.md` | Scoped requirements with unique IDs (REQ-XX) |
| `ROADMAP.md` | Phase breakdown with status tracking and requirement mapping |
| `STATE.md` | Initial project state with position, decisions, metrics |
@@ -171,6 +177,7 @@
- REQ-PLAN-06: System MUST support `--skip-research` flag to bypass research phase
- REQ-PLAN-07: System MUST prompt user to run `/gsd:ui-phase` if frontend phase detected and no UI-SPEC.md exists (UI safety gate)
- REQ-PLAN-08: System MUST include Nyquist validation mapping when `workflow.nyquist_validation` is enabled
- REQ-PLAN-09: System MUST verify all phase requirements are covered by at least one plan before planning completes (requirements coverage gate)
**Produces:**
| Artifact | Description |
@@ -220,6 +227,7 @@
- REQ-EXEC-06: System MUST run post-execution verifier to check phase goals were met
- REQ-EXEC-07: System MUST support git branching strategies (`none`, `phase`, `milestone`)
- REQ-EXEC-08: System MUST invoke node repair operator on task verification failure (when enabled)
- REQ-EXEC-09: System MUST run prior phases' test suites before verification to catch cross-phase regressions
**Produces:**
| Artifact | Description |
@@ -238,9 +246,14 @@
- Reads PLAN.md with full task instructions
- Has access to PROJECT.md, STATE.md, CONTEXT.md, RESEARCH.md
- Commits each task atomically with structured commit messages
- Uses `--no-verify` on commits during parallel execution to avoid build lock contention
- Handles checkpoint types: `auto`, `checkpoint:human-verify`, `checkpoint:decision`, `checkpoint:human-action`
- Reports deviations from plan in SUMMARY.md
**Parallel Safety:**
- **Pre-commit hooks**: Skipped by parallel agents (`--no-verify`), run once by orchestrator after each wave
- **STATE.md locking**: File-level lockfile prevents concurrent write corruption across agents
---
### 6. Work Verification
@@ -261,6 +274,25 @@
---
### 6.5. Ship
**Command:** `/gsd:ship [N] [--draft]`
**Purpose:** Bridge local completion → merged PR. After verification passes, push branch, create PR with auto-generated body from planning artifacts, optionally trigger review, and track in STATE.md.
**Requirements:**
- REQ-SHIP-01: System MUST verify phase has passed verification before shipping
- REQ-SHIP-02: System MUST push branch and create PR via `gh` CLI
- REQ-SHIP-03: System MUST auto-generate PR body from SUMMARY.md, VERIFICATION.md, and REQUIREMENTS.md
- REQ-SHIP-04: System MUST update STATE.md with shipping status and PR number
- REQ-SHIP-05: System MUST support `--draft` flag for draft PRs
**Prerequisites:** Phase verified, `gh` CLI installed and authenticated, work on feature branch
**Produces:** GitHub PR with rich body, STATE.md updated
---
### 7. UI Review
**Command:** `/gsd:ui-review [N]`
@@ -387,9 +419,34 @@
---
### 14. Auto-Advance (Next)
**Command:** `/gsd:next`
**Purpose:** Automatically detect current project state and advance to the next logical workflow step, eliminating the need to remember which phase/step you're on.
**Requirements:**
- REQ-NEXT-01: System MUST read STATE.md, ROADMAP.md, and phase directories to determine current position
- REQ-NEXT-02: System MUST detect whether discuss, plan, execute, or verify is needed
- REQ-NEXT-03: System MUST invoke the correct command automatically
- REQ-NEXT-04: System MUST suggest `/gsd:new-project` if no project exists
- REQ-NEXT-05: System MUST suggest `/gsd:complete-milestone` when all phases are complete
**State Detection Logic:**
| State | Action |
|-------|--------|
| No `.planning/` directory | Suggest `/gsd:new-project` |
| Phase has no CONTEXT.md | Run `/gsd:discuss-phase` |
| Phase has no PLAN.md files | Run `/gsd:plan-phase` |
| Phase has plans but no SUMMARY.md | Run `/gsd:execute-phase` |
| Phase executed but no VERIFICATION.md | Run `/gsd:verify-work` |
| All phases complete | Suggest `/gsd:complete-milestone` |
---
## Quality Assurance Features
### 14. Nyquist Validation
### 15. Nyquist Validation
**Purpose:** Map automated test coverage to phase requirements before any code is written. Named after the Nyquist sampling theorem — ensures a feedback signal exists for every requirement.
@@ -412,7 +469,7 @@
---
### 15. Plan Checking
### 16. Plan Checking
**Purpose:** Goal-backward verification that plans will achieve phase objectives before execution.
@@ -424,7 +481,7 @@
---
### 16. Post-Execution Verification
### 17. Post-Execution Verification
**Purpose:** Automated check that the codebase delivers what the phase promised.
@@ -436,7 +493,7 @@
---
### 17. Node Repair
### 18. Node Repair
**Purpose:** Autonomous recovery when task verification fails during execution.
@@ -450,7 +507,7 @@
---
### 18. Health Validation
### 19. Health Validation
**Command:** `/gsd:health [--repair]`
@@ -465,9 +522,37 @@
---
### 20. Cross-Phase Regression Gate
**Purpose:** Prevent regressions from compounding across phases by running prior phases' test suites after execution.
**Requirements:**
- REQ-REGR-01: System MUST run test suites from all completed prior phases after phase execution
- REQ-REGR-02: System MUST report any test failures as cross-phase regressions
- REQ-REGR-03: Regressions MUST be surfaced before post-execution verification
- REQ-REGR-04: System MUST identify which prior phase's tests were broken
**When:** Runs automatically during `/gsd:execute-phase` before the verifier step.
---
### 21. Requirements Coverage Gate
**Purpose:** Ensure all phase requirements are covered by at least one plan before planning completes.
**Requirements:**
- REQ-COVGATE-01: System MUST extract all requirement IDs assigned to the phase from ROADMAP.md
- REQ-COVGATE-02: System MUST verify each requirement appears in at least one PLAN.md
- REQ-COVGATE-03: Uncovered requirements MUST block planning completion
- REQ-COVGATE-04: System MUST report which specific requirements lack plan coverage
**When:** Runs automatically at the end of `/gsd:plan-phase` after the plan checker loop.
---
## Context Engineering Features
### 19. Context Window Monitoring
### 22. Context Window Monitoring
**Purpose:** Prevent context rot by alerting both user and agent when context is running low.
@@ -487,22 +572,49 @@
---
### 20. Session Management
### 23. Session Management
**Commands:** `/gsd:pause-work`, `/gsd:resume-work`, `/gsd:progress`
**Purpose:** Maintain project continuity across context resets and sessions.
**Requirements:**
- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md`
- REQ-SESSION-02: Resume MUST restore full project context from state files
- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md` and structured `HANDOFF.json`
- REQ-SESSION-02: Resume MUST restore full project context from HANDOFF.json (preferred) or state files (fallback)
- REQ-SESSION-03: Progress MUST show current position, next action, and overall completion
- REQ-SESSION-04: Progress MUST read all state files (STATE.md, ROADMAP.md, phase directories)
- REQ-SESSION-05: All session operations MUST work after `/clear` (context reset)
- REQ-SESSION-06: HANDOFF.json MUST include blockers, human actions pending, and in-progress task state
- REQ-SESSION-07: Resume MUST surface human actions and blockers immediately on session start
---
### 21. Multi-Agent Orchestration
### 24. Session Reporting
**Command:** `/gsd:session-report`
**Purpose:** Generate a structured post-session summary document capturing work performed, outcomes achieved, and estimated resource usage.
**Requirements:**
- REQ-REPORT-01: System MUST gather data from STATE.md, git log, and plan/summary files
- REQ-REPORT-02: System MUST include commits made, plans executed, and phases progressed
- REQ-REPORT-03: System MUST estimate token usage and cost based on session activity
- REQ-REPORT-04: System MUST include active blockers and decisions made
- REQ-REPORT-05: System MUST recommend next steps
**Produces:** `.planning/reports/SESSION_REPORT.md`
**Report Sections:**
- Session overview (duration, milestone, phase)
- Work performed (commits, plans, phases)
- Outcomes and deliverables
- Blockers and decisions
- Resource estimates (tokens, cost)
- Next steps recommendation
---
### 25. Multi-Agent Orchestration
**Purpose:** Coordinate specialized agents with fresh context windows for each task.
@@ -516,7 +628,7 @@
---
### 22. Model Profiles
### 26. Model Profiles
**Command:** `/gsd:set-profile <quality|balanced|budget|inherit>`
@@ -527,6 +639,7 @@
- REQ-MODEL-02: Each profile MUST define model tier per agent (see profile table)
- REQ-MODEL-03: Per-agent overrides MUST take precedence over profile
- REQ-MODEL-04: `inherit` profile MUST defer to runtime's current model selection
- REQ-MODEL-04a: `inherit` profile MUST be used when running non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs
- REQ-MODEL-05: Profile switch MUST be programmatic (script, not LLM-driven)
- REQ-MODEL-06: Model resolution MUST happen once per orchestration, not per spawn
@@ -551,7 +664,7 @@
## Brownfield Features
### 23. Codebase Mapping
### 27. Codebase Mapping
**Command:** `/gsd:map-codebase [area]`
@@ -579,7 +692,7 @@
## Utility Features
### 24. Debug System
### 28. Debug System
**Command:** `/gsd:debug [description]`
@@ -597,7 +710,7 @@
---
### 25. Todo Management
### 29. Todo Management
**Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos`
@@ -611,7 +724,7 @@
---
### 26. Statistics Dashboard
### 30. Statistics Dashboard
**Command:** `/gsd:stats`
@@ -625,7 +738,7 @@
---
### 27. Update System
### 31. Update System
**Command:** `/gsd:update`
@@ -640,7 +753,7 @@
---
### 28. Settings Management
### 32. Settings Management
**Command:** `/gsd:settings`
@@ -673,7 +786,7 @@
---
### 29. Test Generation
### 33. Test Generation
**Command:** `/gsd:add-tests [N]`
@@ -688,7 +801,7 @@
## Infrastructure Features
### 30. Git Integration
### 34. Git Integration
**Purpose:** Atomic commits, branching strategies, and clean history management.
@@ -714,7 +827,7 @@ fix(03-01): correct auth token expiry
---
### 31. CLI Tools
### 35. CLI Tools
**Purpose:** Programmatic utilities for workflows and agents, replacing repetitive inline bash patterns.
@@ -729,7 +842,7 @@ fix(03-01): correct auth token expiry
---
### 32. Multi-Runtime Support
### 36. Multi-Runtime Support
**Purpose:** Run GSD across 6 different AI coding agent runtimes.
@@ -752,7 +865,7 @@ fix(03-01): correct auth token expiry
---
### 33. Hook System
### 37. Hook System
**Purpose:** Runtime event hooks for context monitoring, status display, and update checking.
@@ -772,7 +885,7 @@ fix(03-01): correct auth token expiry
Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
### 33. Developer Profiling
### 38. Developer Profiling
**Command:** `/gsd:profile-user [--questionnaire] [--refresh]`
@@ -808,7 +921,7 @@ Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
- REQ-PROF-03: Questionnaire MUST be available as fallback when no session history exists
- REQ-PROF-04: Generated artifacts MUST be discoverable by Claude Code (CLAUDE.md integration)
### 34. Execution Hardening
### 39. Execution Hardening
**Purpose:** Three additive quality improvements to the execution pipeline that catch cross-plan failures before they cascade.
@@ -827,3 +940,36 @@ After Level 3 wiring verification passes, spot-check individual exports for actu
- REQ-HARD-01: Pre-wave check MUST verify key-links from all prior wave artifacts before spawning next wave
- REQ-HARD-02: Cross-plan contract check MUST detect incompatible data transformations between plans
- REQ-HARD-03: Export spot-check MUST identify dead stores in wired files
---
### 40. Verification Debt Tracking
**Command:** `/gsd:audit-uat`
**Purpose:** Prevent silent loss of UAT/verification items when projects advance past phases with outstanding tests. Surfaces verification debt across all prior phases so items are never forgotten.
**Components:**
**1. Cross-Phase Health Check** (progress.md Step 1.6)
Every `/gsd:progress` call scans ALL phases in the current milestone for outstanding items (pending, skipped, blocked, human_needed). Displays a non-blocking warning section with actionable links.
**2. `status: partial`** (verify-work.md, UAT.md)
New UAT status that distinguishes between "session ended" and "all tests resolved". Prevents `status: complete` when tests are still pending, blocked, or skipped without reason.
**3. `result: blocked` with `blocked_by` tag** (verify-work.md, UAT.md)
New test result type for tests blocked by external dependencies (server, physical device, release build, third-party services). Categorized separately from skipped tests.
**4. HUMAN-UAT.md Persistence** (execute-phase.md)
When verification returns `human_needed`, items are persisted as a trackable HUMAN-UAT.md file with `status: partial`. Feeds into the cross-phase health check and audit systems.
**5. Phase Completion Warnings** (phase.cjs, transition.md)
`phase complete` CLI returns verification debt warnings in its JSON output. Transition workflow surfaces outstanding items before confirmation.
**Requirements:**
- REQ-DEBT-01: System MUST surface outstanding UAT/verification items from ALL prior phases in `/gsd:progress`
- REQ-DEBT-02: System MUST distinguish incomplete testing (partial) from completed testing (complete)
- REQ-DEBT-03: System MUST categorize blocked tests with `blocked_by` tags
- REQ-DEBT-04: System MUST persist human_needed verification items as trackable UAT files
- REQ-DEBT-05: System MUST warn (non-blocking) during phase completion and transition when verification debt exists
- REQ-DEBT-06: `/gsd:audit-uat` MUST scan all phases, categorize items by testability, and produce a human test plan

View File

@@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic
│ │ /gsd:verify-work │ │ <- Manual UAT
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼─────────┐ │
│ │ /gsd:ship │ │ <- Create PR (optional)
│ └──────────┬─────────┘ │
│ │ │
│ Next Phase?────────────┘
│ │ No
└─────────────┼──────────────┘
@@ -284,6 +288,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
| `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase |
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves | After planning is complete |
| `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes |
| `/gsd:ship [N]` | Create PR from verified work | After verification passes |
| `/gsd:next` | Auto-detect state and run next step | Anytime — "what should I do next?" |
| `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) |
| `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone |
| `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified |
@@ -295,7 +301,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
|---------|---------|-------------|
| `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" |
| `/gsd:resume-work` | Restore full context from last session | Starting a new session |
| `/gsd:pause-work` | Save context handoff | Stopping mid-phase |
| `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase |
| `/gsd:session-report` | Generate session summary with work and outcomes | End of session, stakeholder sharing |
| `/gsd:help` | Show all commands | Quick reference |
| `/gsd:update` | Update GSD with changelog preview | Check for new versions |
| `/gsd:join-discord` | Open Discord community invite | Questions or community |
@@ -349,12 +356,13 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
"ui_phase": true,
"ui_safety_gate": true
},
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}"
}
}
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
}
}
```
### Core Settings
@@ -363,7 +371,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|---------|---------|---------|------------------|
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity: how finely scope is sliced (3-5, 5-8, or 8-12 phases) |
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
### Planning Settings
@@ -391,9 +399,10 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
| Setting | Options | Default | What it Controls |
|---------|---------|---------|------------------|
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created |
| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy |
| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy |
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created |
| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy |
| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy |
| `git.quick_branch_template` | Template string or `null` | `null` | Optional branch name for `/gsd:quick` tasks |
**Branching strategies explained:**
@@ -403,29 +412,37 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
| `phase` | At each `execute-phase` | One phase per branch | Code review per phase, granular rollback |
| `milestone` | At first `execute-phase` | All phases share one branch | Release branches, PR per version |
**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0").
**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0"), `{num}` / `{quick}` = quick task ID (e.g., "260317-abc").
Example quick-task branching:
```json
"git": {
"quick_branch_template": "gsd/quick-{num}-{slug}"
}
```
### Model Profiles (Per-Agent Breakdown)
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | Opus | Opus | Sonnet | Inherit |
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | Opus | Opus | Sonnet | Inherit |
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
**Profile philosophy:**
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`).
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **inherit** -- All agents use the current session model. Best when switching models dynamically (e.g. OpenCode `/model`), or **required** when using non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs.
---
@@ -442,12 +459,14 @@ claude --dangerously-skip-permissions
/gsd:plan-phase 1 # Research + plan + verify
/gsd:execute-phase 1 # Parallel execution
/gsd:verify-work 1 # Manual UAT
/gsd:ship 1 # Create PR from verified work
/gsd:ui-review 1 # Visual audit (frontend phases)
/clear
/gsd:discuss-phase 2 # Repeat for each phase
/gsd:next # Auto-detect and run next step
...
/gsd:audit-milestone # Check everything shipped
/gsd:complete-milestone # Archive, tag, done
/gsd:session-report # Generate session summary
```
### New Project from Existing Document
@@ -539,6 +558,10 @@ Do not re-run `/gsd:execute-phase`. Use `/gsd:quick` for targeted fixes, or `/gs
Switch to budget profile: `/gsd:set-profile budget`. Disable research and plan-check agents via `/gsd:settings` if the domain is familiar to you (or to Claude).
### Using Non-Anthropic Models (OpenRouter, Local)
If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd:set-profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd:settings` → Model Profile → Inherit.
### Working on a Sensitive/Private Project
Set `commit_docs: false` during `/gsd:new-project` or via `/gsd:settings`. Add `.planning/` to your `.gitignore`. Planning artifacts stay local and never touch git.
@@ -551,6 +574,21 @@ Since v1.17, the installer backs up locally modified files to `gsd-local-patches
A known workaround exists for a Claude Code classification bug. GSD's orchestrators (execute-phase, quick) spot-check actual output before reporting failure. If you see a failure message but commits were made, check `git log` -- the work may have succeeded.
### Parallel Execution Causes Build Lock Errors
If you see pre-commit hook failures, cargo lock contention, or 30+ minute execution times during parallel wave execution, this is caused by multiple agents triggering build tools simultaneously. GSD handles this automatically since v1.26 — parallel agents use `--no-verify` on commits and the orchestrator runs hooks once after each wave. If you're on an older version, add this to your project's `CLAUDE.md`:
```markdown
## Git Commit Rules for Agents
All subagent/executor commits MUST use `--no-verify`.
```
To disable parallel execution entirely: `/gsd:settings` → set `parallelization.enabled` to `false`.
### Windows: Installation Crashes on Protected Directories
If the installer crashes with `EPERM: operation not permitted, scandir` on Windows, this is caused by OS-protected directories (e.g., Chromium browser profiles). Fixed since v1.24 — update to the latest version. As a workaround, temporarily rename the problematic directory before running the installer.
---
## Recovery Quick Reference
@@ -566,6 +604,9 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat
| Plan doesn't match your vision | `/gsd:discuss-phase [N]` then re-plan |
| Costs running high | `/gsd:set-profile budget` and `/gsd:settings` to toggle agents off |
| Update broke local changes | `/gsd:reapply-patches` |
| Want session summary for stakeholder | `/gsd:session-report` |
| Don't know what step is next | `/gsd:next` |
| Parallel execution build errors | Update GSD or set `parallelization.enabled: false` |
---
@@ -581,7 +622,9 @@ For reference, here is what GSD creates in your project:
STATE.md # Decisions, blockers, session memory
config.json # Workflow configuration
MILESTONES.md # Completed milestone archive
HANDOFF.json # Structured session handoff (from /gsd:pause-work)
research/ # Domain research from /gsd:new-project
reports/ # Session reports (from /gsd:session-report)
todos/
pending/ # Captured ideas awaiting work
done/ # Completed todos

707
docs/zh-CN/README.md Normal file
View File

@@ -0,0 +1,707 @@
<div align="center">
# GET SHIT DONE
**一个轻量级且强大的元提示、上下文工程和规格驱动开发系统,支持 Claude Code、OpenCode、Gemini CLI 和 Codex。**
**解决上下文衰减 —— 即 Claude 填充上下文窗口时发生的质量退化问题。**
[![npm version](https://img.shields.io/npm/v/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc)
[![npm downloads](https://img.shields.io/npm/dm/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc)
[![Tests](https://img.shields.io/github/actions/workflow/status/glittercowboy/get-shit-done/test.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml)
[![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/gsd)
[![X (Twitter)](https://img.shields.io/badge/X-@gsd__foundation-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/gsd_foundation)
[![$GSD Token](https://img.shields.io/badge/$GSD-Dexscreener-1C1C1C?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIgZmlsbD0iIzAwRkYwMCIvPjwvc3ZnPg==&logoColor=00FF00)](https://dexscreener.com/solana/dwudwjvan7bzkw9zwlbyv6kspdlvhwzrqy6ebk8xzxkv)
[![GitHub stars](https://img.shields.io/github/stars/glittercowboy/get-shit-done?style=for-the-badge&logo=github&color=181717)](https://github.com/glittercowboy/get-shit-done)
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
<br>
```bash
npx get-shit-done-cc@latest
```
**支持 Mac、Windows 和 Linux。**
<br>
![GSD Install](../assets/terminal.svg)
<br>
*"如果你清楚自己想要什么,它真的会帮你构建出来。不忽悠。"*
*"我试过 SpecKit、OpenSpec 和 Taskmaster —— 这是我用过的效果最好的。"*
*"这是我用过的 Claude Code 最强大的扩展。没有过度设计。真的就是把事情做完。"*
<br>
**被 Amazon、Google、Shopify 和 Webflow 的工程师信赖使用。**
[我为什么开发这个](#我为什么开发这个) · [工作原理](#工作原理) · [命令](#命令) · [为什么有效](#为什么有效) · [用户指南](USER-GUIDE.md)
</div>
---
## 我为什么开发这个
我是一名独立开发者。我不写代码 —— Claude Code 写。
其他规格驱动开发工具确实存在,比如 BMAD、Speckit... 但它们似乎都把事情搞得比实际需要的复杂得多(冲刺会议、故事点、干系人同步、回顾、Jira 工作流),或者缺乏对你正在构建的东西的真正大局理解。我不是一个 50 人的软件公司。我不想搞企业级表演。我只是个想构建出好用的东西的创意人。
所以我开发了 GSD。复杂性在系统内部,不在你的工作流里。幕后是:上下文工程、XML 提示格式、子代理编排、状态管理。你看到的是:几个命令,用就完了。
系统给 Claude 提供了它完成工作**以及**验证工作所需的一切。我信任这个工作流。它就是做得好。
这就是它的本质。没有企业级角色扮演的废话。只是一个让 Claude Code 稳定可靠地构建酷东西的极其有效的系统。
— **TÂCHES**
---
Vibecoding 名声不好。你描述想要什么,AI 生成代码,结果得到不一致的垃圾,规模一大就崩。
GSD 解决了这个问题。它是让 Claude Code 变得可靠的上下文工程层。描述你的想法,让系统提取它需要知道的一切,然后让 Claude Code 开始工作。
---
## 这个工具适合谁
想要描述需求然后正确构建出来的人 —— 不用假装自己在运营一个 50 人的工程组织。
---
## 快速开始
```bash
npx get-shit-done-cc@latest
```
安装程序会提示你选择:
1. **运行时** —— Claude Code、OpenCode、Gemini、Codex 或全部
2. **位置** —— 全局(所有项目)或本地(仅当前项目)
验证安装:
- Claude Code / Gemini: `/gsd:help`
- OpenCode: `/gsd-help`
- Codex: `$gsd-help`
> [!NOTE]
> Codex 安装使用技能(`skills/gsd-*/SKILL.md`)而非自定义提示。
### 保持更新
GSD 快速迭代。定期更新:
```bash
npx get-shit-done-cc@latest
```
<details>
<summary><strong>非交互式安装(Docker、CI、脚本)</strong></summary>
```bash
# Claude Code
npx get-shit-done-cc --claude --global # 安装到 ~/.claude/
npx get-shit-done-cc --claude --local # 安装到 ./.claude/
# OpenCode(开源,免费模型)
npx get-shit-done-cc --opencode --global # 安装到 ~/.config/opencode/
# Gemini CLI
npx get-shit-done-cc --gemini --global # 安装到 ~/.gemini/
# Codex(技能优先)
npx get-shit-done-cc --codex --global # 安装到 ~/.codex/
npx get-shit-done-cc --codex --local # 安装到 ./.codex/
# 所有运行时
npx get-shit-done-cc --all --global # 安装到所有目录
```
使用 `--global`(`-g`)或 `--local`(`-l`)跳过位置提示。
使用 `--claude`、`--opencode`、`--gemini`、`--codex` 或 `--all` 跳过运行时提示。
</details>
<details>
<summary><strong>开发安装</strong></summary>
克隆仓库并本地运行安装程序:
```bash
git clone https://github.com/glittercowboy/get-shit-done.git
cd get-shit-done
node bin/install.js --claude --local
```
安装到 `./.claude/` 用于在贡献前测试修改。
</details>
### 推荐:跳过权限模式
GSD 设计为无摩擦自动化。运行 Claude Code 时使用:
```bash
claude --dangerously-skip-permissions
```
> [!TIP]
> 这是 GSD 的预期使用方式 —— 停下来 50 次批准 `date` 和 `git commit` 会失去意义。
<details>
<summary><strong>替代方案:细粒度权限</strong></summary>
如果你不想使用那个标志,在项目的 `.claude/settings.json` 中添加:
```json
{
"permissions": {
"allow": [
"Bash(date:*)",
"Bash(echo:*)",
"Bash(cat:*)",
"Bash(ls:*)",
"Bash(mkdir:*)",
"Bash(wc:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(sort:*)",
"Bash(grep:*)",
"Bash(tr:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git status:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git tag:*)"
]
}
}
```
</details>
---
## 工作原理
> **已有代码?** 先运行 `/gsd:map-codebase`。它会生成并行代理分析你的技术栈、架构、约定和关注点。然后 `/gsd:new-project` 就了解你的代码库了 —— 问题聚焦在你正在**添加**什么,规划会自动加载你的模式。
### 1. 初始化项目
```
/gsd:new-project
```
一条命令,一个流程。系统:
1. **提问** —— 问到完全理解你的想法为止(目标、约束、技术偏好、边缘情况)
2. **研究** —— 生成并行代理调查领域(可选但推荐)
3. **需求** —— 提取哪些是 v1、v2 和范围外
4. **路线图** —— 创建映射到需求的阶段
你批准路线图。现在准备好构建了。
**创建:** `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md`、`.planning/research/`
---
### 2. 讨论阶段
```
/gsd:discuss-phase 1
```
**这是你塑造实现方式的地方。**
你的路线图每个阶段有一两句话。这不足以按照**你**想象的方式构建东西。这一步在研究或规划之前捕获你的偏好。
系统分析阶段并根据正在构建的内容识别灰色区域:
- **视觉功能** → 布局、密度、交互、空状态
- **API/CLI** → 响应格式、标志、错误处理、详细程度
- **内容系统** → 结构、语气、深度、流程
- **组织任务** → 分组标准、命名、重复项、例外
对于你选择的每个领域,它会问到让你满意为止。输出 —— `CONTEXT.md` —— 直接输入接下来的两个步骤:
1. **研究员读取它** —— 知道要调查什么模式("用户想要卡片布局" → 研究卡片组件库)
2. **规划者读取它** —— 知道哪些决策已锁定("无限滚动已决定" → 规划包含滚动处理)
你在这里走得越深,系统构建的就越是你真正想要的。跳过它你会得到合理的默认值。使用它你会得到**你的**愿景。
**创建:** `{阶段号}-CONTEXT.md`
---
### 3. 规划阶段
```
/gsd:plan-phase 1
```
系统:
1. **研究** —— 调查如何实现这个阶段,由你的 CONTEXT.md 决策指导
2. **规划** —— 创建 2-3 个带有 XML 结构的原子任务计划
3. **验证** —— 根据需求检查计划,循环直到通过
每个计划足够小,可以在全新的上下文窗口中执行。没有退化,没有"我现在会更简洁"。
**创建:** `{阶段号}-RESEARCH.md`、`{阶段号}-{N}-PLAN.md`
---
### 4. 执行阶段
```
/gsd:execute-phase 1
```
系统:
1. **按波次运行计划** —— 可能的话并行,有依赖时顺序
2. **每个计划全新上下文** —— 200k token 纯粹用于实现,零累积垃圾
3. **每个任务提交** —— 每个任务都有自己的原子提交
4. **根据目标验证** —— 检查代码库是否交付了阶段承诺的内容
离开,回来看到完成的工作和干净的 git 历史。
**波次执行工作原理:**
计划根据依赖关系分组到"波次"。在每个波次内,计划并行运行。波次顺序执行。
```
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段执行 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 波次 1 (并行) 波次 2 (并行) 波次 3 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 计划 01 │ │ 计划 02 │ → │ 计划 03 │ │ 计划 04 │ → │ 计划 05 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ 用户 │ │ 产品 │ │ 订单 │ │ 购物车 │ │ 结账 │ │
│ │ 模型 │ │ 模型 │ │ API │ │ API │ │ UI │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ ↑ ↑ ↑ │
│ └───────────┴──────────────┴───────────┘ │ │
│ 依赖关系: 计划 03 需要计划 01 │ │
│ 计划 04 需要计划 02 │ │
│ 计划 05 需要计划 03 + 04 │ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**为什么波次重要:**
- 独立计划 → 同一波次 → 并行运行
- 依赖计划 → 后续波次 → 等待依赖
- 文件冲突 → 顺序计划或同一计划
这就是为什么"垂直切片"(计划 01: 用户功能端到端)比"水平分层"(计划 01: 所有模型,计划 02: 所有 API)并行化更好。
**创建:** `{阶段号}-{N}-SUMMARY.md`、`{阶段号}-VERIFICATION.md`
---
### 5. 验证工作
```
/gsd:verify-work 1
```
**这是你确认它真的有效的地方。**
自动化验证检查代码存在和测试通过。但功能是否按你预期的方式**工作**?这是你使用它的机会。
系统:
1. **提取可测试交付物** —— 你现在应该能做什么
2. **逐个引导你** —— "你能用邮箱登录吗?" 是/否,或描述有什么问题
3. **自动诊断失败** —— 生成调试代理找根本原因
4. **创建已验证的修复计划** —— 准备立即重新执行
如果一切通过,继续。如果有东西坏了,不用手动调试 —— 只需再次运行 `/gsd:execute-phase`,使用它创建的修复计划。
**创建:** `{阶段号}-UAT.md`,如果发现问题则创建修复计划
---
### 6. 循环 → 完成 → 下一个里程碑
```
/gsd:discuss-phase 2
/gsd:plan-phase 2
/gsd:execute-phase 2
/gsd:verify-work 2
...
/gsd:complete-milestone
/gsd:new-milestone
```
循环 **讨论 → 规划 → 执行 → 验证** 直到里程碑完成。
如果你想在讨论期间更快速地输入,使用 `/gsd:discuss-phase <n> --batch` 一次回答一组小问题,而不是一个一个来。
每个阶段都会获得你的输入(讨论)、适当的研究(规划)、干净的执行(执行)和人工验证(验证)。上下文保持新鲜。质量保持高水平。
当所有阶段完成后,`/gsd:complete-milestone` 归档里程碑并标记发布。
然后 `/gsd:new-milestone` 开始下一个版本 —— 与 `new-project` 相同的流程,但针对你现有的代码库。你描述接下来想构建什么,系统研究领域,你界定需求范围,它创建新的路线图。每个里程碑是一个干净的周期:定义 → 构建 → 发布。
---
### 快速模式
```
/gsd:quick
```
**用于不需要完整规划的临时任务。**
快速模式给你 GSD 保证(原子提交、状态跟踪)和更快的路径:
- **相同代理** —— 规划者 + 执行者,相同质量
- **跳过可选步骤** —— 无研究、无计划检查器、无验证器
- **独立跟踪** —— 存放在 `.planning/quick/`,不是阶段
用于:bug 修复、小功能、配置更改、一次性任务。
```
/gsd:quick
> 你想做什么?"在设置中添加深色模式切换"
```
**创建:** `.planning/quick/001-add-dark-mode-toggle/PLAN.md`、`SUMMARY.md`
---
## 为什么有效
### 上下文工程
Claude Code 非常强大,**如果你**给它需要的上下文。大多数人没有。
GSD 为你处理:
| 文件 | 作用 |
|------|------|
| `PROJECT.md` | 项目愿景,始终加载 |
| `research/` | 生态知识(技术栈、功能、架构、陷阱) |
| `REQUIREMENTS.md` | 界定 v1/v2 需求及阶段可追溯性 |
| `ROADMAP.md` | 你要去哪里,完成了什么 |
| `STATE.md` | 决策、阻塞项、位置 —— 跨会话记忆 |
| `PLAN.md` | 带有 XML 结构和验证步骤的原子任务 |
| `SUMMARY.md` | 发生了什么,改了什么,提交到历史 |
| `todos/` | 为后续工作捕获的想法和任务 |
基于 Claude 质量退化的位置设置大小限制。保持在限制内,获得一致的卓越。
### XML 提示格式
每个计划都是为 Claude 优化的结构化 XML:
```xml
<task type="auto">
<name>创建登录端点</name>
<files>src/app/api/auth/login/route.ts</files>
<action>
使用 jose 处理 JWT(不用 jsonwebtoken - CommonJS 问题)。
根据 users 表验证凭据。
成功时返回 httpOnly cookie。
</action>
<verify>curl -X POST localhost:3000/api/auth/login 返回 200 + Set-Cookie</verify>
<done>有效凭据返回 cookie,无效返回 401</done>
</task>
```
精确的指令。不猜测。内置验证。
### 多代理编排
每个阶段使用相同模式:轻量编排器生成专门代理,收集结果,路由到下一步。
| 阶段 | 编排器做 | 代理做 |
|-------|------------------|-----------|
| 研究 | 协调,呈现发现 | 4 个并行研究员调查技术栈、功能、架构、陷阱 |
| 规划 | 验证,管理迭代 | 规划者创建计划,检查器验证,循环直到通过 |
| 执行 | 分组为波次,跟踪进度 | 执行者并行实现,每个有全新 200k 上下文 |
| 验证 | 呈现结果,路由下一步 | 验证器根据目标检查代码库,调试器诊断失败 |
编排器从不做重活。它生成代理,等待,整合结果。
**结果:** 你可以运行整个阶段 —— 深度研究、多个计划创建和验证、跨并行执行者编写数千行代码、根据目标自动化验证 —— 你的主上下文窗口保持在 30-40%。工作在全新的子代理上下文中完成。你的会话保持快速和响应。
### 原子 Git 提交
每个任务在完成后立即获得自己的提交:
```bash
abc123f docs(08-02): 完成用户注册计划
def456g feat(08-02): 添加邮箱确认流程
hij789k feat(08-02): 实现密码哈希
lmn012o feat(08-02): 创建注册端点
```
> [!NOTE]
> **好处:** Git bisect 找到确切的失败任务。每个任务独立可回滚。未来会话中 Claude 的清晰历史。AI 自动化工作流中更好的可观察性。
每个提交都是精确的、可追溯的、有意义的。
### 模块化设计
- 向当前里程碑添加阶段
- 在阶段之间插入紧急工作
- 完成里程碑并重新开始
- 调整计划而不重建一切
你永远不会被锁定。系统会适应。
---
## 命令
### 核心工作流
| 命令 | 作用 |
|---------|--------------|
| `/gsd:new-project [--auto]` | 完整初始化:提问 → 研究 → 需求 → 路线图 |
| `/gsd:discuss-phase [N] [--auto]` | 在规划前捕获实现决策 |
| `/gsd:plan-phase [N] [--auto]` | 阶段的研究 + 规划 + 验证 |
| `/gsd:execute-phase <N>` | 在并行波次中执行所有计划,完成后验证 |
| `/gsd:verify-work [N]` | 手动用户验收测试 ¹ |
| `/gsd:audit-milestone` | 验证里程碑达到了其完成定义 |
| `/gsd:complete-milestone` | 归档里程碑,标记发布 |
| `/gsd:new-milestone [name]` | 开始下一个版本:提问 → 研究 → 需求 → 路线图 |
### 导航
| 命令 | 作用 |
|---------|--------------|
| `/gsd:progress` | 我在哪?接下来做什么? |
| `/gsd:help` | 显示所有命令和使用指南 |
| `/gsd:update` | 更新 GSD 并预览变更日志 |
| `/gsd:join-discord` | 加入 GSD Discord 社区 |
### 现有代码库
| 命令 | 作用 |
|---------|--------------|
| `/gsd:map-codebase` | 在 new-project 之前分析现有代码库 |
### 阶段管理
| 命令 | 作用 |
|---------|--------------|
| `/gsd:add-phase` | 向路线图追加阶段 |
| `/gsd:insert-phase [N]` | 在阶段之间插入紧急工作 |
| `/gsd:remove-phase [N]` | 删除未来阶段,重新编号 |
| `/gsd:list-phase-assumptions [N]` | 规划前查看 Claude 的预期方法 |
| `/gsd:plan-milestone-gaps` | 创建阶段以填补审计发现的差距 |
### 会话
| 命令 | 作用 |
|---------|--------------|
| `/gsd:pause-work` | 阶段中途停止时创建交接 |
| `/gsd:resume-work` | 从上次会话恢复 |
### 工具
| 命令 | 作用 |
|---------|--------------|
| `/gsd:settings` | 配置模型配置文件和工作流代理 |
| `/gsd:set-profile <profile>` | 切换模型配置文件(quality/balanced/budget) |
| `/gsd:add-todo [desc]` | 捕获想法留待后用 |
| `/gsd:check-todos` | 列出待处理事项 |
| `/gsd:debug [desc]` | 带持久状态的系统化调试 |
| `/gsd:quick [--full] [--discuss]` | 用 GSD 保证执行临时任务(`--full` 添加计划检查和验证,`--discuss` 先收集上下文) |
| `/gsd:health [--repair]` | 验证 `.planning/` 目录完整性,用 `--repair` 自动修复 |
<sup>¹ 由 Reddit 用户 OracleGreyBeard 贡献</sup>
---
## 配置
GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。完整配置模式、工作流开关、git 分支选项和每个代理的模型分解,请参阅[用户指南](USER-GUIDE.md#配置参考)。
### 核心设置
| 设置 | 选项 | 默认值 | 控制内容 |
|---------|---------|---------|------------------|
| `mode` | `yolo`, `interactive` | `interactive` | 自动批准 vs 每步确认 |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度 —— 范围切分多细(阶段 × 计划) |
### 模型配置
控制每个代理使用哪个 Claude 模型。平衡质量和 token 消耗。
| 配置 | 规划 | 执行 | 验证 |
|---------|----------|-----------|--------------|
| `quality` | Opus | Opus | Sonnet |
| `balanced`(默认) | Opus | Sonnet | Sonnet |
| `budget` | Sonnet | Sonnet | Haiku |
切换配置:
```
/gsd:set-profile budget
```
或通过 `/gsd:settings` 配置。
### 工作流代理
这些在规划/执行期间生成额外代理。它们提高质量但增加 token 和时间。
| 设置 | 默认值 | 作用 |
|---------|---------|--------------|
| `workflow.research` | `true` | 每个阶段规划前研究领域 |
| `workflow.plan_check` | `true` | 执行前验证计划是否达到阶段目标 |
| `workflow.verifier` | `true` | 执行后确认必须项已交付 |
| `workflow.auto_advance` | `false` | 自动链式执行 讨论 → 规划 → 执行 |
使用 `/gsd:settings` 切换这些,或每次调用时覆盖:
- `/gsd:plan-phase --skip-research`
- `/gsd:plan-phase --skip-verify`
### 执行
| 设置 | 默认值 | 控制内容 |
|---------|---------|------------------|
| `parallelization.enabled` | `true` | 同时运行独立计划 |
| `planning.commit_docs` | `true` | 在 git 中跟踪 `.planning/` |
### Git 分支
控制 GSD 在执行期间如何处理分支。
| 设置 | 选项 | 默认值 | 作用 |
|---------|---------|---------|--------------|
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 分支创建策略 |
| `git.phase_branch_template` | 字符串 | `gsd/phase-{phase}-{slug}` | 阶段分支模板 |
| `git.milestone_branch_template` | 字符串 | `gsd/{milestone}-{slug}` | 里程碑分支模板 |
**策略:**
- **`none`** —— 提交到当前分支(默认 GSD 行为)
- **`phase`** —— 每个阶段创建一个分支,阶段完成时合并
- **`milestone`** —— 为整个里程碑创建一个分支,完成时合并
在里程碑完成时,GSD 提供 squash 合并(推荐)或带历史合并。
---
## 安全
### 保护敏感文件
GSD 的代码库映射和分析命令读取文件以了解你的项目。**保护包含密钥的文件**,将它们添加到 Claude Code 的拒绝列表:
1. 打开 Claude Code 设置(`.claude/settings.json` 或全局)
2. 将敏感文件模式添加到拒绝列表:
```json
{
"permissions": {
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/secrets/*)",
"Read(**/*credential*)",
"Read(**/*.pem)",
"Read(**/*.key)"
]
}
}
```
这完全阻止 Claude 读取这些文件,无论你运行什么命令。
> [!IMPORTANT]
> GSD 包含内置保护以防止提交密钥,但纵深防御是最佳实践。拒绝读取敏感文件作为第一道防线。
---
## 故障排除
**安装后找不到命令?**
- 重启运行时以重新加载命令/技能
- 验证文件是否存在于 `~/.claude/commands/gsd/`(全局)或 `./.claude/commands/gsd/`(本地)
- 对于 Codex,验证技能是否存在于 `~/.codex/skills/gsd-*/SKILL.md`(全局)或 `./.codex/skills/gsd-*/SKILL.md`(本地)
**命令没有按预期工作?**
- 运行 `/gsd:help` 验证安装
- 重新运行 `npx get-shit-done-cc` 重新安装
**更新到最新版本?**
```bash
npx get-shit-done-cc@latest
```
**使用 Docker 或容器化环境?**
如果用波浪号路径(`~/.claude/...`)读取文件失败,在安装前设置 `CLAUDE_CONFIG_DIR`:
```bash
CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global
```
这确保使用绝对路径而不是 `~`,后者在容器中可能无法正确展开。
### 卸载
完全删除 GSD:
```bash
# 全局安装
npx get-shit-done-cc --claude --global --uninstall
npx get-shit-done-cc --opencode --global --uninstall
npx get-shit-done-cc --codex --global --uninstall
# 本地安装(当前项目)
npx get-shit-done-cc --claude --local --uninstall
npx get-shit-done-cc --opencode --local --uninstall
npx get-shit-done-cc --codex --local --uninstall
```
这删除所有 GSD 命令、代理、钩子和设置,同时保留你的其他配置。
---
## 社区移植
OpenCode、Gemini CLI 和 Codex 现在通过 `npx get-shit-done-cc` 原生支持。
这些社区移植开创了多运行时支持:
| 项目 | 平台 | 描述 |
|---------|----------|-------------|
| [gsd-opencode](https://github.com/rokicool/gsd-opencode) | OpenCode | 原始 OpenCode 适配 |
| gsd-gemini (已归档) | Gemini CLI | 由 uberfuzzy 开发的原始 Gemini 适配 |
---
## Star 历史
<a href="https://star-history.com/#glittercowboy/get-shit-done&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date" />
</picture>
</a>
---
## 许可证
MIT 许可证。详见 [LICENSE](../LICENSE)。
---
<div align="center">
**Claude Code 很强大。GSD 让它可靠。**
</div>

492
docs/zh-CN/USER-GUIDE.md Normal file
View File

@@ -0,0 +1,492 @@
# GSD 用户指南
工作流、故障排除和配置的详细参考。快速入门设置请参阅 [README](README.md)。
---
## 目录
- [工作流图解](#工作流图解)
- [命令参考](#命令参考)
- [配置参考](#配置参考)
- [使用示例](#使用示例)
- [故障排除](#故障排除)
- [恢复快速参考](#恢复快速参考)
---
## 工作流图解
### 完整项目生命周期
```
┌──────────────────────────────────────────────────┐
│ 新建项目 │
│ /gsd:new-project │
│ 提问 -> 研究 -> 需求 -> 路线图 │
└─────────────────────────┬────────────────────────┘
│
┌──────────────▼─────────────┐
│ 每个阶段: │
│ │
│ ┌────────────────────┐ │
│ │ /gsd:discuss-phase │ │ <- 锁定偏好
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼─────────┐ │
│ │ /gsd:plan-phase │ │ <- 研究 + 规划 + 验证
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼─────────┐ │
│ │ /gsd:execute-phase │ │ <- 并行执行
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼─────────┐ │
│ │ /gsd:verify-work │ │ <- 手动 UAT
│ └──────────┬─────────┘ │
│ │ │
│ 下一阶段?────────────┘
│ │ 否
└─────────────┼──────────────┘
│
┌───────────────▼──────────────┐
│ /gsd:audit-milestone │
│ /gsd:complete-milestone │
└───────────────┬──────────────┘
│
另一个里程碑?
│ │
是 否 -> 完成!
│
┌───────▼──────────────┐
│ /gsd:new-milestone │
└──────────────────────┘
```
### 规划代理协调
```
/gsd:plan-phase N
│
├── 阶段研究员 (x4 并行)
│ ├── 技术栈研究员
│ ├── 功能研究员
│ ├── 架构研究员
│ └── 陷阱研究员
│ │
│ ┌──────▼──────┐
│ │ RESEARCH.md │
│ └──────┬──────┘
│ │
│ ┌──────▼──────┐
│ │ 规划者 │ <- 读取 PROJECT.md, REQUIREMENTS.md,
│ │ │ CONTEXT.md, RESEARCH.md
│ └──────┬──────┘
│ │
│ ┌──────▼───────────┐ ┌────────┐
│ │ 计划检查器 │────>│ 通过? │
│ └──────────────────┘ └───┬────┘
│ │
│ 是 │ 否
│ │ │ │
│ │ └───┘ (循环,最多 3 次)
│ │
│ ┌─────▼──────┐
│ │ PLAN 文件 │
│ └────────────┘
└── 完成
```
### 验证架构 (Nyquist 层)
在 plan-phase 研究期间,GSD 现在在任何代码编写之前将自动化测试覆盖率映射到每个阶段需求。这确保当 Claude 的执行者提交任务时,反馈机制已经存在可以在几秒钟内验证它。
研究员检测你现有的测试基础设施,将每个需求映射到特定的测试命令,并识别在实现开始之前必须创建的任何测试脚手架(波次 0 任务)。
计划检查器将其强制作为第 8 个验证维度:缺少自动化验证命令的计划将不会被批准。
**输出:** `{阶段}-VALIDATION.md` —— 阶段的反馈契约。
**禁用:** 在 `/gsd:settings` 中设置 `workflow.nyquist_validation: false`,用于测试基础设施不是重点的快速原型阶段。
### 追溯验证 (`/gsd:validate-phase`)
对于在 Nyquist 验证存在之前执行的阶段,或只有传统测试套件的现有代码库,追溯审计并填补覆盖缺口:
```
/gsd:validate-phase N
|
+-- 检测状态 (VALIDATION.md 存在? SUMMARY.md 存在?)
|
+-- 发现: 扫描实现,将需求映射到测试
|
+-- 分析缺口: 哪些需求缺少自动化验证?
|
+-- 呈现缺口计划供审批
|
+-- 生成审计器: 生成测试,运行,调试(最多 3 次尝试)
|
+-- 更新 VALIDATION.md
|
+-- COMPLIANT -> 所有需求都有自动化检查
+-- PARTIAL -> 部分缺口升级为仅手动
```
审计器从不修改实现代码 —— 只修改测试文件和 VALIDATION.md。如果测试发现实现 bug,它会标记为升级让你处理。
**何时使用:** 在启用了 Nyquist 之前规划的阶段执行后,或在 `/gsd:audit-milestone` 发现 Nyquist 合规缺口后。
### 执行波次协调
```
/gsd:execute-phase N
│
├── 分析计划依赖
│
├── 波次 1 (独立计划):
│ ├── 执行者 A (全新 200K 上下文) -> 提交
│ └── 执行者 B (全新 200K 上下文) -> 提交
│
├── 波次 2 (依赖波次 1):
│ └── 执行者 C (全新 200K 上下文) -> 提交
│
└── 验证器
└── 根据阶段目标检查代码库
│
├── 通过 -> VERIFICATION.md (成功)
└── 失败 -> 问题记录到 /gsd:verify-work
```
### 现有代码库工作流
```
/gsd:map-codebase
│
├── 技术栈映射器 -> codebase/STACK.md
├── 架构映射器 -> codebase/ARCHITECTURE.md
├── 约定映射器 -> codebase/CONVENTIONS.md
└── 关注点映射器 -> codebase/CONCERNS.md
│
┌───────▼──────────┐
│ /gsd:new-project │ <- 问题聚焦于你正在添加的内容
└──────────────────┘
```
---
## 命令参考
### 核心工作流
| 命令 | 用途 | 何时使用 |
|---------|---------|-------------|
| `/gsd:new-project` | 完整项目初始化:提问、研究、需求、路线图 | 新项目开始时 |
| `/gsd:new-project --auto @idea.md` | 从文档自动初始化 | 有现成的 PRD 或想法文档 |
| `/gsd:discuss-phase [N]` | 捕获实现决策 | 规划前,塑造构建方式 |
| `/gsd:plan-phase [N]` | 研究 + 规划 + 验证 | 执行阶段前 |
| `/gsd:execute-phase <N>` | 在并行波次中执行所有计划 | 规划完成后 |
| `/gsd:verify-work [N]` | 带自动诊断的手动 UAT | 执行完成后 |
| `/gsd:audit-milestone` | 验证里程碑达到其完成定义 | 完成里程碑前 |
| `/gsd:complete-milestone` | 归档里程碑,标记发布 | 所有阶段已验证 |
| `/gsd:new-milestone [name]` | 开始下一个版本周期 | 完成里程碑后 |
### 导航
| 命令 | 用途 | 何时使用 |
|---------|---------|-------------|
| `/gsd:progress` | 显示状态和下一步 | 任何时候 -- "我在哪?" |
| `/gsd:resume-work` | 从上次会话恢复完整上下文 | 开始新会话 |
| `/gsd:pause-work` | 保存上下文交接 | 阶段中途停止 |
| `/gsd:help` | 显示所有命令 | 快速参考 |
| `/gsd:update` | 更新 GSD 并预览变更日志 | 检查新版本 |
| `/gsd:join-discord` | 打开 Discord 社区邀请 | 问题或社区 |
### 阶段管理
| 命令 | 用途 | 何时使用 |
|---------|---------|-------------|
| `/gsd:add-phase` | 向路线图追加新阶段 | 初始规划后范围增长 |
| `/gsd:insert-phase [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 |
| `/gsd:remove-phase [N]` | 删除未来阶段并重新编号 | 移除某个功能 |
| `/gsd:list-phase-assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 |
| `/gsd:plan-milestone-gaps` | 为审计缺口创建阶段 | 审计发现缺失项后 |
| `/gsd:research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 |
### 现有代码库和工具
| 命令 | 用途 | 何时使用 |
|---------|---------|-------------|
| `/gsd:map-codebase` | 分析现有代码库 | 在现有代码上运行 `/gsd:new-project` 之前 |
| `/gsd:quick` | 带 GSD 保证的临时任务 | Bug 修复、小功能、配置更改 |
| `/gsd:debug [desc]` | 带持久状态的系统化调试 | 出问题时 |
| `/gsd:add-todo [desc]` | 捕获想法留待后用 | 会话期间想到什么 |
| `/gsd:check-todos` | 列出待处理事项 | 查看捕获的想法 |
| `/gsd:settings` | 配置工作流开关和模型配置 | 更改模型、切换代理 |
| `/gsd:set-profile <profile>` | 快速切换配置 | 更改成本/质量权衡 |
| `/gsd:reapply-patches` | 更新后恢复本地修改 | 如果你有本地编辑,在 `/gsd:update` 后 |
---
## 配置参考
GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。
### 完整 config.json 模式
```json
{
"mode": "interactive",
"granularity": "standard",
"model_profile": "balanced",
"planning": {
"commit_docs": true,
"search_gitignored": false
},
"workflow": {
"research": true,
"plan_check": true,
"verifier": true,
"nyquist_validation": true
},
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}"
}
}
```
### 核心设置
| 设置 | 选项 | 默认值 | 控制内容 |
|---------|---------|---------|------------------|
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` 自动批准决策;`interactive` 每步确认 |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度:范围切分多细(3-5、5-8 或 8-12 个阶段) |
| `model_profile` | `quality`, `balanced`, `budget` | `balanced` | 每个代理的模型层级(见下表) |
### 规划设置
| 设置 | 选项 | 默认值 | 控制内容 |
|---------|---------|---------|------------------|
| `planning.commit_docs` | `true`, `false` | `true` | `.planning/` 文件是否提交到 git |
| `planning.search_gitignored` | `true`, `false` | `false` | 在广泛搜索中添加 `--no-ignore` 以包含 `.planning/` |
> **注意:** 如果 `.planning/` 在 `.gitignore` 中,无论配置值如何,`commit_docs` 自动为 `false`。
### 工作流开关
| 设置 | 选项 | 默认值 | 控制内容 |
|---------|---------|---------|------------------|
| `workflow.research` | `true`, `false` | `true` | 规划前的领域调查 |
| `workflow.plan_check` | `true`, `false` | `true` | 计划验证循环(最多 3 次迭代) |
| `workflow.verifier` | `true`, `false` | `true` | 根据阶段目标的执行后验证 |
| `workflow.nyquist_validation` | `true`, `false` | `true` | plan-phase 期间的验证架构研究;第 8 个计划检查维度 |
在熟悉的领域或需要节省 token 时禁用这些以加速阶段。
### Git 分支
| 设置 | 选项 | 默认值 | 控制内容 |
|---------|---------|---------|------------------|
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 何时以及如何创建分支 |
| `git.phase_branch_template` | 模板字符串 | `gsd/phase-{phase}-{slug}` | 阶段策略的分支名 |
| `git.milestone_branch_template` | 模板字符串 | `gsd/{milestone}-{slug}` | 里程碑策略的分支名 |
**分支策略说明:**
| 策略 | 创建分支 | 范围 | 适用于 |
|----------|---------------|-------|----------|
| `none` | 从不 | N/A | 独立开发、简单项目 |
| `phase` | 每次 `execute-phase` | 每个阶段一个分支 | 每阶段代码审查、细粒度回滚 |
| `milestone` | 第一次 `execute-phase` | 所有阶段共享一个分支 | 发布分支、每个版本一个 PR |
**模板变量:** `{phase}` = 零填充数字(如 "03"),`{slug}` = 小写连字符名称,`{milestone}` = 版本(如 "v1.0")。
### 模型配置(每个代理分解)
| 代理 | `quality` | `balanced` | `budget` |
|-------|-----------|------------|----------|
| gsd-planner | Opus | Opus | Sonnet |
| gsd-roadmapper | Opus | Sonnet | Sonnet |
| gsd-executor | Opus | Sonnet | Sonnet |
| gsd-phase-researcher | Opus | Sonnet | Haiku |
| gsd-project-researcher | Opus | Sonnet | Haiku |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku |
| gsd-debugger | Opus | Sonnet | Sonnet |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku |
| gsd-verifier | Sonnet | Sonnet | Haiku |
| gsd-plan-checker | Sonnet | Sonnet | Haiku |
| gsd-integration-checker | Sonnet | Sonnet | Haiku |
**配置理念:**
- **quality** —— 所有决策代理使用 Opus,只读验证使用 Sonnet。有配额可用且工作关键时使用。
- **balanced** —— 仅规划(架构决策发生的地方)使用 Opus,其他全部使用 Sonnet。这是默认,有充分理由。
- **budget** —— 编写代码的使用 Sonnet,研究和验证使用 Haiku。大量工作或不太关键的阶段使用。
---
## 使用示例
### 新项目(完整周期)
```bash
claude --dangerously-skip-permissions
/gsd:new-project # 回答问题,配置,批准路线图
/clear
/gsd:discuss-phase 1 # 锁定你的偏好
/gsd:plan-phase 1 # 研究 + 规划 + 验证
/gsd:execute-phase 1 # 并行执行
/gsd:verify-work 1 # 手动 UAT
/clear
/gsd:discuss-phase 2 # 对每个阶段重复
...
/gsd:audit-milestone # 检查所有内容已发布
/gsd:complete-milestone # 归档,标记,完成
```
### 从现有文档创建新项目
```bash
/gsd:new-project --auto @prd.md # 从你的文档自动运行研究/需求/路线图
/clear
/gsd:discuss-phase 1 # 从这里开始正常流程
```
### 现有代码库
```bash
/gsd:map-codebase # 分析现有内容(并行代理)
/gsd:new-project # 问题聚焦于你正在添加的内容
# (从这里开始正常阶段工作流)
```
### 快速 Bug 修复
```bash
/gsd:quick
> "修复移动端 Safari 上登录按钮无响应的问题"
```
### 中断后恢复
```bash
/gsd:progress # 查看你停在哪和接下来做什么
# 或
/gsd:resume-work # 从上次会话完整恢复上下文
```
### 准备发布
```bash
/gsd:audit-milestone # 检查需求覆盖率,检测存根
/gsd:plan-milestone-gaps # 如果审计发现缺口,创建阶段来填补
/gsd:complete-milestone # 归档,标记,完成
```
### 速度与质量预设
| 场景 | 模式 | 粒度 | 配置 | 研究 | 计划检查 | 验证器 |
|----------|------|-------|---------|----------|------------|----------|
| 原型开发 | `yolo` | `coarse` | `budget` | 关 | 关 | 关 |
| 正常开发 | `interactive` | `standard` | `balanced` | 开 | 开 | 开 |
| 生产环境 | `interactive` | `fine` | `quality` | 开 | 开 | 开 |
### 里程碑中途范围变更
```bash
/gsd:add-phase # 向路线图追加新阶段
# 或
/gsd:insert-phase 3 # 在阶段 3 和 4 之间插入紧急工作
# 或
/gsd:remove-phase 7 # 移除阶段 7 并重新编号
```
---
## 故障排除
### "项目已初始化"
你运行了 `/gsd:new-project` 但 `.planning/PROJECT.md` 已存在。这是安全检查。如果你想重新开始,先删除 `.planning/` 目录。
### 长会话期间上下文退化
在主要命令之间清除上下文窗口:Claude Code 中的 `/clear`。GSD 设计围绕全新上下文 —— 每个子代理获得干净的 200K 窗口。如果主会话质量下降,清除并使用 `/gsd:resume-work` 或 `/gsd:progress` 恢复状态。
### 计划看起来错误或不一致
在规划前运行 `/gsd:discuss-phase [N]`。大多数计划质量问题来自 Claude 做出了 `CONTEXT.md` 本可以防止的假设。你也可以运行 `/gsd:list-phase-assumptions [N]` 在提交计划前查看 Claude 打算做什么。
### 执行失败或产生存根
检查计划是否太雄心勃勃。计划最多应有 2-3 个任务。如果任务太大,它们超出了单个上下文窗口可以可靠产生的内容。用更小的范围重新规划。
### 忘记你在哪里
运行 `/gsd:progress`。它读取所有状态文件,准确告诉你位置和下一步。
### 执行后需要更改某些内容
不要重新运行 `/gsd:execute-phase`。使用 `/gsd:quick` 进行针对性修复,或用 `/gsd:verify-work` 通过 UAT 系统识别和修复问题。
### 模型成本太高
切换到 budget 配置:`/gsd:set-profile budget`。如果领域对你(或 Claude)熟悉,通过 `/gsd:settings` 禁用研究和计划检查代理。
### 处理敏感/私有项目
在 `/gsd:new-project` 期间或通过 `/gsd:settings` 设置 `commit_docs: false`。将 `.planning/` 添加到 `.gitignore`。规划工件保留在本地,从不接触 git。
### GSD 更新覆盖了我的本地更改
从 v1.17 开始,安装程序将本地修改的文件备份到 `gsd-local-patches/`。运行 `/gsd:reapply-patches` 将你的更改合并回来。
### 子代理似乎失败但工作已完成
存在 Claude Code 分类 bug 的已知解决方法。GSD 的编排器(execute-phase、quick)在报告失败前抽查实际输出。如果你看到失败消息但提交已创建,检查 `git log` —— 工作可能已成功。
---
## 恢复快速参考
| 问题 | 解决方案 |
|---------|----------|
| 丢失上下文 / 新会话 | `/gsd:resume-work` 或 `/gsd:progress` |
| 阶段出错 | `git revert` 阶段提交,然后重新规划 |
| 需要更改范围 | `/gsd:add-phase`、`/gsd:insert-phase` 或 `/gsd:remove-phase` |
| 里程碑审计发现缺口 | `/gsd:plan-milestone-gaps` |
| 出问题了 | `/gsd:debug "描述"` |
| 快速针对性修复 | `/gsd:quick` |
| 计划与你的愿景不符 | `/gsd:discuss-phase [N]` 然后重新规划 |
| 成本过高 | `/gsd:set-profile budget` 和 `/gsd:settings` 关闭代理 |
| 更新破坏了本地更改 | `/gsd:reapply-patches` |
---
## 项目文件结构
供参考,这是 GSD 在你的项目中创建的内容:
```
.planning/
PROJECT.md # 项目愿景和上下文(始终加载)
REQUIREMENTS.md # 界定 v1/v2 需求及 ID
ROADMAP.md # 带状态跟踪的阶段分解
STATE.md # 决策、阻塞项、会话记忆
config.json # 工作流配置
MILESTONES.md # 已完成里程碑归档
research/ # 来自 /gsd:new-project 的领域研究
todos/
pending/ # 等待处理的捕获想法
done/ # 已完成的待办事项
debug/ # 活跃调试会话
resolved/ # 已归档的调试会话
codebase/ # 现有代码库映射(来自 /gsd:map-codebase)
phases/
XX-phase-name/
XX-YY-PLAN.md # 原子执行计划
XX-YY-SUMMARY.md # 执行结果和决策
CONTEXT.md # 你的实现偏好
RESEARCH.md # 生态研究发现
VERIFICATION.md # 执行后验证结果
```

View File

@@ -0,0 +1,450 @@
# 检查点
计划自主执行。检查点用于规范化需要人工验证或决策的交互点。
**核心原则:** Claude 用 CLI/API 自动化一切。检查点用于验证和决策,而非手动工作。
**黄金法则:**
1. **如果 Claude 能运行,Claude 就运行** - 绝不让用户执行 CLI 命令、启动服务器或运行构建
2. **Claude 设置验证环境** - 启动开发服务器、填充数据库、配置环境变量
3. **用户只做需要人工判断的事** - 视觉检查、UX 评估、"这个感觉对吗?"
4. **密钥来自用户,自动化来自 Claude** - 询问 API 密钥,然后 Claude 通过 CLI 使用它们
5. **自动模式绕过验证/决策检查点** — 当 config 中 `workflow._auto_chain_active` 或 `workflow.auto_advance` 为 true 时:human-verify 自动批准,decision 自动选择第一个选项,human-action 仍会停止(认证门控无法自动化)
## 检查点类型
### checkpoint:human-verify(最常见 - 90%)
**何时使用:** Claude 完成自动化工作,人工确认其正常工作。
**用于:**
- 视觉 UI 检查(布局、样式、响应式)
- 交互流程(点击向导、测试用户流程)
- 功能验证(功能按预期工作)
- 音频/视频播放质量
- 动画流畅度
- 无障碍测试
**结构:**
```xml
<task type="checkpoint:human-verify" gate="blocking">
<what-built>[Claude 自动化并部署/构建的内容]</what-built>
<how-to-verify>
[测试的确切步骤 - URL、命令、预期行为]
</how-to-verify>
<resume-signal>[如何继续 - "approved"、"yes" 或描述问题]</resume-signal>
</task>
```
**示例:UI 组件(展示关键模式:Claude 在检查点之前启动服务器)**
```xml
<task type="auto">
<name>构建响应式仪表板布局</name>
<files>src/components/Dashboard.tsx, src/app/dashboard/page.tsx</files>
<action>创建带侧边栏、标题和内容区域的仪表板。使用 Tailwind 响应式类处理移动端。</action>
<verify>npm run build 成功,无 TypeScript 错误</verify>
<done>仪表板组件构建无错误</done>
</task>
<task type="auto">
<name>启动开发服务器用于验证</name>
<action>在后台运行 `npm run dev`,等待 "ready" 消息,捕获端口</action>
<verify>curl http://localhost:3000 返回 200</verify>
<done>开发服务器运行于 http://localhost:3000</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>响应式仪表板布局 - 开发服务器运行于 http://localhost:3000</what-built>
<how-to-verify>
访问 http://localhost:3000/dashboard 并验证:
1. 桌面端 (>1024px): 左侧边栏,右侧内容,顶部标题
2. 平板端 (768px): 侧边栏折叠为汉堡菜单
3. 移动端 (375px): 单列布局,出现底部导航
4. 任何尺寸无布局偏移或水平滚动
</how-to-verify>
<resume-signal>输入 "approved" 或描述布局问题</resume-signal>
</task>
```
### checkpoint:decision(9%)
**何时使用:** 人工必须做出影响实现方向的选择。
**用于:**
- 技术选型(哪个认证提供商、哪个数据库)
- 架构决策(monorepo 还是独立仓库)
- 设计选择(配色方案、布局方式)
- 功能优先级(构建哪个变体)
- 数据模型决策(模式结构)
**结构:**
```xml
<task type="checkpoint:decision" gate="blocking">
<decision>[正在决策的内容]</decision>
<context>[为什么这个决策重要]</context>
<options>
<option id="option-a">
<name>[选项名称]</name>
<pros>[好处]</pros>
<cons>[权衡]</cons>
</option>
<option id="option-b">
<name>[选项名称]</name>
<pros>[好处]</pros>
<cons>[权衡]</cons>
</option>
</options>
<resume-signal>[如何表明选择]</resume-signal>
</task>
```
**示例:认证提供商选择**
```xml
<task type="checkpoint:decision" gate="blocking">
<decision>选择认证提供商</decision>
<context>
应用需要用户认证。三个可靠选项各有权衡。
</context>
<options>
<option id="supabase">
<name>Supabase Auth</name>
<pros>与我们使用的 Supabase DB 内置集成,慷慨的免费额度,行级安全集成</pros>
<cons>UI 定制性较差,绑定 Supabase 生态</cons>
</option>
<option id="clerk">
<name>Clerk</name>
<pros>精美的预构建 UI,最佳开发体验,优秀文档</pros>
<cons>10k MAU 后付费,供应商锁定</cons>
</option>
<option id="nextauth">
<name>NextAuth.js</name>
<pros>免费,自托管,最大控制权,广泛采用</pros>
<cons>更多设置工作,需自行管理安全更新,UI 需自己构建</cons>
</option>
</options>
<resume-signal>选择:supabase、clerk 或 nextauth</resume-signal>
</task>
```
### checkpoint:human-action(1% - 罕见)
**何时使用:** 操作没有 CLI/API 且需要仅人工交互,或者 Claude 在自动化过程中遇到认证门控。
**仅用于:**
- **认证门控** - Claude 尝试了 CLI/API 但需要凭证(这不是失败)
- 邮箱验证链接(点击邮件)
- 短信两步验证码(手机验证)
- 人工账户审批(平台需要人工审核)
- 信用卡 3D Secure 流程(基于 Web 的支付授权)
- OAuth 应用审批(基于 Web 的审批)
**不要用于预定的手动工作:**
- 部署(使用 CLI - 如需要则认证门控)
- 创建 webhooks/数据库(使用 API/CLI - 如需要则认证门控)
- 运行构建/测试(使用 Bash 工具)
- 创建文件(使用 Write 工具)
**结构:**
```xml
<task type="checkpoint:human-action" gate="blocking">
<action>[人工必须做什么 - Claude 已完成所有可自动化的]</action>
<instructions>
[Claude 已自动化的内容]
[需要人工操作的一件事]
</instructions>
<verification>[Claude 之后可以检查的内容]</verification>
<resume-signal>[如何继续]</resume-signal>
</task>
```
**示例:认证门控(动态检查点)**
```xml
<task type="auto">
<name>部署到 Vercel</name>
<files>.vercel/, vercel.json</files>
<action>运行 `vercel --yes` 进行部署</action>
<verify>vercel ls 显示部署,curl 返回 200</verify>
</task>
<!-- 如果 vercel 返回 "Error: Not authenticated",Claude 即时创建检查点 -->
<task type="checkpoint:human-action" gate="blocking">
<action>认证 Vercel CLI 以便我继续部署</action>
<instructions>
我尝试部署但收到认证错误。
运行:vercel login
这将打开你的浏览器 - 完成认证流程。
</instructions>
<verification>vercel whoami 返回你的账户邮箱</verification>
<resume-signal>认证完成后输入 "done"</resume-signal>
</task>
<!-- 认证后,Claude 重试部署 -->
<task type="auto">
<name>重试 Vercel 部署</name>
<action>运行 `vercel --yes`(已认证)</action>
<verify>vercel ls 显示部署,curl 返回 200</verify>
</task>
```
**关键区别:** 认证门控是 Claude 遇到认证错误时动态创建的。不是预定的 — Claude 先自动化,只有在被阻止时才请求凭证。
## 执行协议
当 Claude 遇到 `type="checkpoint:*"` 时:
1. **立即停止** - 不继续下一个任务
2. **清晰显示检查点** 使用下面的格式
3. **等待用户响应** - 不幻想完成
4. **如可能则验证** - 检查文件、运行测试、任何指定的内容
5. **恢复执行** - 仅在确认后继续下一个任务
**对于 checkpoint:human-verify:**
```
╔═══════════════════════════════════════════════════════╗
║ CHECKPOINT: 需要验证 ║
╚═══════════════════════════════════════════════════════╝
进度: 5/8 任务完成
任务: 响应式仪表板布局
已构建: /dashboard 的响应式仪表板
如何验证:
1. 访问: http://localhost:3000/dashboard
2. 桌面端 (>1024px): 侧边栏可见,内容填充剩余空间
3. 平板端 (768px): 侧边栏折叠为图标
4. 移动端 (375px): 侧边栏隐藏,出现汉堡菜单
────────────────────────────────────────────────────────
→ 你的操作: 输入 "approved" 或描述问题
────────────────────────────────────────────────────────
```
**对于 checkpoint:decision:**
```
╔═══════════════════════════════════════════════════════╗
║ CHECKPOINT: 需要决策 ║
╚═══════════════════════════════════════════════════════╝
进度: 2/6 任务完成
任务: 选择认证提供商
决策: 我们应该使用哪个认证提供商?
上下文: 需要用户认证。三个选项各有权衡。
选项:
1. supabase - 与我们的数据库内置集成,免费额度
优点: 行级安全集成,慷慨的免费额度
缺点: UI 定制性较差,生态锁定
2. clerk - 最佳 DX,10k 用户后付费
优点: 精美的预构建 UI,优秀文档
缺点: 供应商锁定,规模化时价格问题
3. nextauth - 自托管,最大控制权
优点: 免费,无供应商锁定,广泛采用
缺点: 更多设置工作,自行 DIY 安全更新
────────────────────────────────────────────────────────
→ 你的操作: 选择 supabase、clerk 或 nextauth
────────────────────────────────────────────────────────
```
## 认证门控
**认证门控 = Claude 尝试了 CLI/API,收到认证错误。** 不是失败 — 是需要人工输入来解除阻止的门控。
**模式:** Claude 尝试自动化 → 认证错误 → 创建 checkpoint:human-action → 用户认证 → Claude 重试 → 继续
**门控协议:**
1. 认识到这不是失败 - 缺少认证是正常的
2. 停止当前任务 - 不要反复重试
3. 动态创建 checkpoint:human-action
4. 提供确切的认证步骤
5. 验证认证有效
6. 重试原始任务
7. 正常继续
**关键区别:**
- 预定的检查点:"我需要你做 X"(错误 - Claude 应该自动化)
- 认证门控:"我尝试自动化 X 但需要凭证"(正确 - 解除自动化阻止)
## 自动化参考
**规则:** 如果有 CLI/API,Claude 就做。绝不让人工执行可自动化的工作。
### 服务 CLI 参考
| 服务 | CLI/API | 关键命令 | 认证门控 |
|------|---------|----------|----------|
| Vercel | `vercel` | `--yes`, `env add`, `--prod`, `ls` | `vercel login` |
| Railway | `railway` | `init`, `up`, `variables set` | `railway login` |
| Fly | `fly` | `launch`, `deploy`, `secrets set` | `fly auth login` |
| Stripe | `stripe` + API | `listen`, `trigger`, API 调用 | .env 中的 API key |
| Supabase | `supabase` | `init`, `link`, `db push`, `gen types` | `supabase login` |
| Upstash | `upstash` | `redis create`, `redis get` | `upstash auth login` |
| PlanetScale | `pscale` | `database create`, `branch create` | `pscale auth login` |
| GitHub | `gh` | `repo create`, `pr create`, `secret set` | `gh auth login` |
| Node | `npm`/`pnpm` | `install`, `run build`, `test`, `run dev` | N/A |
| Xcode | `xcodebuild` | `-project`, `-scheme`, `build`, `test` | N/A |
| Convex | `npx convex` | `dev`, `deploy`, `env set`, `env get` | `npx convex login` |
### 环境变量自动化
**Env 文件:** 使用 Write/Edit 工具。绝不让用户手动创建 .env。
**通过 CLI 的仪表板环境变量:**
| 平台 | CLI 命令 | 示例 |
|------|----------|------|
| Convex | `npx convex env set` | `npx convex env set OPENAI_API_KEY sk-...` |
| Vercel | `vercel env add` | `vercel env add STRIPE_KEY production` |
| Railway | `railway variables set` | `railway variables set API_KEY=value` |
| Fly | `fly secrets set` | `fly secrets set DATABASE_URL=...` |
| Supabase | `supabase secrets set` | `supabase secrets set MY_SECRET=value` |
### 开发服务器自动化
| 框架 | 启动命令 | 就绪信号 | 默认 URL |
|------|----------|----------|----------|
| Next.js | `npm run dev` | "Ready in" 或 "started server" | http://localhost:3000 |
| Vite | `npm run dev` | "ready in" | http://localhost:5173 |
| Convex | `npx convex dev` | "Convex functions ready" | N/A(仅后端)|
| Express | `npm start` | "listening on port" | http://localhost:3000 |
| Django | `python manage.py runserver` | "Starting development server" | http://localhost:8000 |
**服务器生命周期:**
```bash
# 后台运行,捕获 PID
npm run dev &
DEV_SERVER_PID=$!
# 等待就绪(最多 30s)
timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done'
```
**端口冲突:** 终止陈旧进程(`lsof -ti:3000 | xargs kill`)或使用备用端口(`--port 3001`)。
**服务器保持运行** 直到检查点结束。仅在计划完成、切换到生产环境或端口需要用于不同服务时终止。
### CLI 安装处理
| CLI | 自动安装? | 命令 |
|-----|------------|------|
| npm/pnpm/yarn | 否 - 询问用户 | 用户选择包管理器 |
| vercel | 是 | `npm i -g vercel` |
| gh (GitHub) | 是 | `brew install gh` (macOS) 或 `apt install gh` (Linux) |
| stripe | 是 | `npm i -g stripe` |
| supabase | 是 | `npm i -g supabase` |
| convex | 否 - 使用 npx | `npx convex`(无需安装)|
| fly | 是 | `brew install flyctl` 或 curl 安装器 |
| railway | 是 | `npm i -g @railway/cli` |
**协议:** 尝试命令 → "command not found" → 可自动安装?→ 是:静默安装,重试 → 否:检查点请求用户安装。
## 检查点前自动化失败处理
| 失败 | 响应 |
|------|------|
| 服务器无法启动 | 检查错误,修复问题,重试(不进入检查点)|
| 端口被占用 | 终止陈旧进程或使用备用端口 |
| 缺少依赖 | 运行 `npm install`,重试 |
| 构建错误 | 先修复错误(是 bug,不是检查点问题)|
| 认证错误 | 创建认证门控检查点 |
| 网络超时 | 带退避重试,如果持续则检查点 |
**绝不呈现验证环境损坏的检查点。** 如果 `curl localhost:3000` 失败,不要让用户"访问 localhost:3000"。
## 可自动化快速参考
| 操作 | 可自动化?| Claude 做?|
|------|------------|------------|
| 部署到 Vercel | 是 (`vercel`) | 是 |
| 创建 Stripe webhook | 是 (API) | 是 |
| 写入 .env 文件 | 是 (Write 工具) | 是 |
| 创建 Upstash DB | 是 (`upstash`) | 是 |
| 运行测试 | 是 (`npm test`) | 是 |
| 启动开发服务器 | 是 (`npm run dev`) | 是 |
| 添加环境变量到 Convex | 是 (`npx convex env set`) | 是 |
| 添加环境变量到 Vercel | 是 (`vercel env add`) | 是 |
| 填充数据库 | 是 (CLI/API) | 是 |
| 点击邮件验证链接 | 否 | 否 |
| 输入带 3DS 的信用卡 | 否 | 否 |
| 在浏览器中完成 OAuth | 否 | 否 |
| 视觉验证 UI 是否正确 | 否 | 否 |
| 测试交互式用户流程 | 否 | 否 |
## 反模式
### ❌ 错误:让用户启动开发服务器
```xml
<task type="checkpoint:human-verify" gate="blocking">
<what-built>仪表板组件</what-built>
<how-to-verify>
1. 运行: npm run dev
2. 访问: http://localhost:3000/dashboard
3. 检查布局是否正确
</how-to-verify>
</task>
```
**为什么错误:** Claude 可以运行 `npm run dev`。用户应该只访问 URL,不执行命令。
### ✅ 正确:Claude 启动服务器,用户访问
```xml
<task type="auto">
<name>启动开发服务器</name>
<action>在后台运行 `npm run dev`</action>
<verify>curl localhost:3000 返回 200</verify>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>http://localhost:3000/dashboard 的仪表板(服务器运行中)</what-built>
<how-to-verify>
访问 http://localhost:3000/dashboard 并验证:
1. 布局匹配设计
2. 无控制台错误
</how-to-verify>
</task>
```
### ❌ 错误:让用户部署 / ✅ 正确:Claude 自动化
```xml
<!-- 错误:让用户通过仪表板部署 -->
<task type="checkpoint:human-action" gate="blocking">
<action>部署到 Vercel</action>
<instructions>访问 vercel.com/new → 导入仓库 → 点击部署 → 复制 URL</instructions>
</task>
<!-- 正确:Claude 部署,用户验证 -->
<task type="auto">
<name>部署到 Vercel</name>
<action>运行 `vercel --yes`。捕获 URL。</action>
<verify>vercel ls 显示部署,curl 返回 200</verify>
</task>
<task type="checkpoint:human-verify">
<what-built>已部署到 {url}</what-built>
<how-to-verify>访问 {url},检查首页加载</how-to-verify>
<resume-signal>输入 "approved"</resume-signal>
</task>
```
## 摘要
检查点规范化人工介入点用于验证和决策,而非手动工作。
**黄金法则:** 如果 Claude 能自动化它,Claude 就必须自动化它。
**检查点优先级:**
1. **checkpoint:human-verify**(90%)- Claude 自动化一切,人工确认视觉/功能正确性
2. **checkpoint:decision**(9%)- 人工做出架构/技术选择
3. **checkpoint:human-action**(1%)- 真正无法避免的、没有 API/CLI 的手动步骤
**何时不用检查点:**
- Claude 可以编程验证的事情(测试、构建)
- 文件操作(Claude 可以读取文件)
- 代码正确性(测试和静态分析)
- 任何可通过 CLI/API 自动化的内容

View File

@@ -0,0 +1,249 @@
# 续接格式
完成命令或工作流后展示下一步的标准格式。
## 核心结构
```
---
## ▶ 下一步
**{标识符}: {名称}** — {单行描述}
`{可复制粘贴的命令}`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
**也可选:**
- `{备选项 1}` — 描述
- `{备选项 2}` — 描述
---
```
## 格式规则
1. **始终展示它是什么** — 名称 + 描述,绝不仅仅是一个命令路径
2. **从源文件拉取上下文** — ROADMAP.md 用于阶段,PLAN.md `<objective>` 用于计划
3. **命令用内联代码** — 反引号,易于复制粘贴,渲染为可点击链接
4. **`/clear` 说明** — 始终包含,保持简洁但解释原因
5. **用"也可选"而非"其他选项"** — 听起来更像应用
6. **视觉分隔符** — 上下用 `---` 使其突出
## 变体
### 执行下一个计划
```
---
## ▶ 下一步
**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh
`/gsd:execute-phase 2`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
**也可选:**
- 执行前审查计划
- `/gsd:list-phase-assumptions 2` — 检查假设
---
```
### 执行阶段中最后一个计划
添加注释说明这是最后一个计划以及接下来是什么:
```
---
## ▶ 下一步
**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh
<sub>阶段 2 的最后一个计划</sub>
`/gsd:execute-phase 2`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
**完成后:**
- 阶段 2 → 阶段 3 过渡
- 下一步:**阶段 3: 核心功能** — 用户仪表板和设置
---
```
### 规划阶段
```
---
## ▶ 下一步
**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程
`/gsd:plan-phase 2`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
**也可选:**
- `/gsd:discuss-phase 2` — 先收集上下文
- `/gsd:research-phase 2` — 调查未知项
- 审查路线图
---
```
### 阶段完成,准备下一步
在下一步操作前显示完成状态:
```
---
## ✓ 阶段 2 完成
3/3 计划已执行
## ▶ 下一步
**阶段 3: 核心功能** — 用户仪表板、设置和数据导出
`/gsd:plan-phase 3`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
**也可选:**
- `/gsd:discuss-phase 3` — 先收集上下文
- `/gsd:research-phase 3` — 调查未知项
- 回顾阶段 2 构建的内容
---
```
### 多个同等选项
当没有明确的主要操作时:
```
---
## ▶ 下一步
**阶段 3: 核心功能** — 用户仪表板、设置和数据导出
**直接规划:** `/gsd:plan-phase 3`
**先讨论上下文:** `/gsd:discuss-phase 3`
**研究未知项:** `/gsd:research-phase 3`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
```
### 里程碑完成
```
---
## 🎉 里程碑 v1.0 完成
全部 4 个阶段已发布
## ▶ 下一步
**开始 v1.1** — 提问 → 研究 → 需求 → 路线图
`/gsd:new-milestone`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
---
```
## 拉取上下文
### 用于阶段(从 ROADMAP.md):
```markdown
### 阶段 2: 认证
**目标**: 带刷新令牌的 JWT 登录流程
```
提取:`**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程`
### 用于计划(从 ROADMAP.md):
```markdown
计划:
- [ ] 02-03: 添加刷新令牌轮换
```
或从 PLAN.md `<objective>`:
```xml
<objective>
添加带滑动过期窗口的刷新令牌轮换。
目的: 在不影响安全性的前提下延长会话生命周期。
</objective>
```
提取:`**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh`
## 反模式
### 不要:仅命令(无上下文)
```
## 继续
运行 `/clear`,然后粘贴:
/gsd:execute-phase 2
```
用户不知道 02-03 是关于什么的。
### 不要:缺少 /clear 说明
```
`/gsd:plan-phase 3`
先运行 /clear。
```
没有解释原因。用户可能跳过。
### 不要:"其他选项" 措辞
```
其他选项:
- 审查路线图
```
听起来像是事后补充。用"也可选:"替代。
### 不要:用围栏代码块展示命令
```
```
/gsd:plan-phase 3
```
```
模板内的围栏代码块会造成嵌套歧义。用内联反引号替代。

View File

@@ -0,0 +1,65 @@
# 小数阶段计算
为紧急插入计算下一个小数阶段编号。
## 使用 gsd-tools
```bash
# 获取阶段 6 之后的下一个小数阶段
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 6
```
输出:
```json
{
"found": true,
"base_phase": "06",
"next": "06.1",
"existing": []
}
```
已有小数时:
```json
{
"found": true,
"base_phase": "06",
"next": "06.3",
"existing": ["06.1", "06.2"]
}
```
## 提取值
```bash
DECIMAL_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}")
DECIMAL_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.next')
BASE_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.base_phase')
```
或使用 --raw 标志:
```bash
DECIMAL_PHASE=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}" --raw)
# 返回: 06.1
```
## 示例
| 已有阶段 | 下一个阶段 |
|----------|------------|
| 仅 06 | 06.1 |
| 06, 06.1 | 06.2 |
| 06, 06.1, 06.2 | 06.3 |
| 06, 06.1, 06.3(有空缺)| 06.4 |
## 目录命名
小数阶段目录使用完整的小数编号:
```bash
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$DESCRIPTION" --raw)
PHASE_DIR=".planning/phases/${DECIMAL_PHASE}-${SLUG}"
mkdir -p "$PHASE_DIR"
```
示例:`.planning/phases/06.1-fix-critical-auth-bug/`

View File

@@ -0,0 +1,248 @@
<overview>
GSD 框架的 Git 集成。
</overview>
<core_principle>
**提交结果,而非过程。**
git 日志应该读起来像是发布内容的变更日志,而不是规划活动的日记。
</core_principle>
<commit_points>
| 事件 | 提交? | 原因 |
| ----------------------- | ------- | ------------------------------------------------ |
| BRIEF + ROADMAP 创建 | 是 | 项目初始化 |
| PLAN.md 创建 | 否 | 中间产物 - 与计划完成一起提交 |
| RESEARCH.md 创建 | 否 | 中间产物 |
| DISCOVERY.md 创建 | 否 | 中间产物 |
| **任务完成** | 是 | 原子工作单元(每个任务 1 个提交) |
| **计划完成** | 是 | 元数据提交(SUMMARY + STATE + ROADMAP) |
| 交接创建 | 是 | WIP 状态保留 |
</commit_points>
<git_check>
```bash
[ -d .git ] && echo "GIT_EXISTS" || echo "NO_GIT"
```
如果 NO_GIT:静默运行 `git init`。GSD 项目总是有自己的仓库。
</git_check>
<commit_formats>
<format name="initialization">
## 项目初始化(brief + roadmap 一起)
```
docs: initialize [project-name] ([N] phases)
[PROJECT.md 中的一句话描述]
Phases:
1. [phase-name]: [goal]
2. [phase-name]: [goal]
3. [phase-name]: [goal]
```
提交内容:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [project-name] ([N] phases)" --files .planning/
```
</format>
<format name="task-completion">
## 任务完成(计划执行期间)
每个任务在完成后立即获得自己的提交。
```
{type}({phase}-{plan}): {task-name}
- [关键变更 1]
- [关键变更 2]
- [关键变更 3]
```
**提交类型:**
- `feat` - 新功能/功能
- `fix` - Bug 修复
- `test` - 仅测试(TDD RED 阶段)
- `refactor` - 代码清理(TDD REFACTOR 阶段)
- `perf` - 性能改进
- `chore` - 依赖、配置、工具
**示例:**
```bash
# 标准任务
git add src/api/auth.ts src/types/user.ts
git commit -m "feat(08-02): create user registration endpoint
- POST /auth/register validates email and password
- Checks for duplicate users
- Returns JWT token on success
"
# TDD 任务 - RED 阶段
git add src/__tests__/jwt.test.ts
git commit -m "test(07-02): add failing test for JWT generation
- Tests token contains user ID claim
- Tests token expires in 1 hour
- Tests signature verification
"
# TDD 任务 - GREEN 阶段
git add src/utils/jwt.ts
git commit -m "feat(07-02): implement JWT generation
- Uses jose library for signing
- Includes user ID and expiry claims
- Signs with HS256 algorithm
"
```
</format>
<format name="plan-completion">
## 计划完成(所有任务完成后)
所有任务提交后,最后一个元数据提交捕获计划完成。
```
docs({phase}-{plan}): complete [plan-name] plan
Tasks completed: [N]/[N]
- [Task 1 name]
- [Task 2 name]
- [Task 3 name]
SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md
```
提交内容:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
```
**注意:** 代码文件不包含 - 已按任务提交。
</format>
<format name="handoff">
## 交接(WIP)
```
wip: [phase-name] paused at task [X]/[Y]
Current: [task name]
[如果阻塞:] Blocked: [reason]
```
提交内容:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
```
</format>
</commit_formats>
<example_log>
**旧方法(每个计划提交):**
```
a7f2d1 feat(checkout): Stripe payments with webhook verification
3e9c4b feat(products): catalog with search, filters, and pagination
8a1b2c feat(auth): JWT with refresh rotation using jose
5c3d7e feat(foundation): Next.js 15 + Prisma + Tailwind scaffold
2f4a8d docs: initialize ecommerce-app (5 phases)
```
**新方法(每个任务提交):**
```
# Phase 04 - Checkout
1a2b3c docs(04-01): complete checkout flow plan
4d5e6f feat(04-01): add webhook signature verification
7g8h9i feat(04-01): implement payment session creation
0j1k2l feat(04-01): create checkout page component
# Phase 03 - Products
3m4n5o docs(03-02): complete product listing plan
6p7q8r feat(03-02): add pagination controls
9s0t1u feat(03-02): implement search and filters
2v3w4x feat(03-01): create product catalog schema
# Phase 02 - Auth
5y6z7a docs(02-02): complete token refresh plan
8b9c0d feat(02-02): implement refresh token rotation
1e2f3g test(02-02): add failing test for token refresh
4h5i6j docs(02-01): complete JWT setup plan
7k8l9m feat(02-01): add JWT generation and validation
0n1o2p chore(02-01): install jose library
# Phase 01 - Foundation
3q4r5s docs(01-01): complete scaffold plan
6t7u8v feat(01-01): configure Tailwind and globals
9w0x1y feat(01-01): set up Prisma with database
2z3a4b feat(01-01): create Next.js 15 project
# Initialization
5c6d7e docs: initialize ecommerce-app (5 phases)
```
每个计划产生 2-4 个提交(任务 + 元数据)。清晰、细粒度、可 bisect。
</example_log>
<anti_patterns>
**仍不要提交(中间产物):**
- PLAN.md 创建(与计划完成一起提交)
- RESEARCH.md(中间产物)
- DISCOVERY.md(中间产物)
- 小的规划调整
- "Fixed typo in roadmap"
**要提交(结果):**
- 每个任务完成(feat/fix/test/refactor)
- 计划完成元数据(docs)
- 项目初始化(docs)
**关键原则:** 提交可工作的代码和已发布的结果,而非规划过程。
</anti_patterns>
<commit_strategy_rationale>
## 为什么使用每任务提交?
**AI 上下文工程:**
- Git 历史成为未来 Claude 会话的主要上下文源
- `git log --grep="{phase}-{plan}"` 显示计划的所有工作
- `git diff <hash>^..<hash>` 显示每个任务的确切变更
- 减少对解析 SUMMARY.md 的依赖 = 更多上下文用于实际工作
**失败恢复:**
- 任务 1 已提交 ✅,任务 2 失败 ❌
- 下次会话中的 Claude:看到任务 1 完成,可以重试任务 2
- 可以 `git reset --hard` 到最后一个成功的任务
**调试:**
- `git bisect` 找到确切的失败任务,而不仅仅是失败计划
- `git blame` 将行追溯到特定任务上下文
- 每个提交独立可回滚
**可观察性:**
- 独立开发者 + Claude 工作流受益于细粒度归因
- 原子提交是 git 最佳实践
- 当消费者是 Claude 而非人类时,"提交噪音"无关紧要
</commit_strategy_rationale>

View File

@@ -0,0 +1,38 @@
# Git 规划提交
使用 gsd-tools CLI 提交规划工件,它会自动检查 `commit_docs` 配置和 gitignore 状态。
## 通过 CLI 提交
始终使用 `gsd-tools.cjs commit` 处理 `.planning/` 文件 — 它会自动处理 `commit_docs` 和 gitignore 检查:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
```
如果 `commit_docs` 为 `false` 或 `.planning/` 被 gitignore,CLI 会返回 `skipped`(带原因)。无需手动条件检查。
## 修改上次提交
将 `.planning/` 文件变更合并到上次提交:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "" --files .planning/codebase/*.md --amend
```
## 提交消息模式
| 命令 | 范围 | 示例 |
|------|------|------|
| plan-phase | phase | `docs(phase-03): create authentication plans` |
| execute-phase | phase | `docs(phase-03): complete authentication phase` |
| new-milestone | milestone | `docs: start milestone v1.1` |
| remove-phase | chore | `chore: remove phase 17 (dashboard)` |
| insert-phase | phase | `docs: insert phase 16.1 (critical fix)` |
| add-phase | phase | `docs: add phase 07 (settings page)` |
## 何时跳过
- config 中 `commit_docs: false`
- `.planning/` 被 gitignore
- 无变更可提交(用 `git status --porcelain .planning/` 检查)

View File

@@ -0,0 +1,34 @@
# 模型配置解析
在编排开始时解析一次模型配置,然后在所有 Task 生成时使用。
## 解析模式
```bash
MODEL_PROFILE=$(cat .planning/config.json 2>/dev/null | grep -o '"model_profile"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"' || echo "balanced")
```
默认值:未设置或缺少 config 时为 `balanced`。
## 查找表
@~/.claude/get-shit-done/references/model-profiles.md
在表中查找已解析配置对应的代理。将 model 参数传递给 Task 调用:
```
Task(
prompt="...",
subagent_type="gsd-planner",
model="{resolved_model}" # "inherit"、"sonnet" 或 "haiku"
)
```
**注意:** Opus 级代理解析为 `"inherit"`(而非 `"opus"`)。这会使代理使用父会话的模型,避免与可能阻止特定 opus 版本的组织策略冲突。
## 使用方法
1. 在编排开始时解析一次
2. 存储 profile 值
3. 生成时在表中查找每个代理的模型
4. 将 model 参数传递给每个 Task 调用(值:`"inherit"`、`"sonnet"`、`"haiku"`)

View File

@@ -0,0 +1,93 @@
# 模型配置
模型配置控制每个 GSD 代理使用哪个 Claude 模型。这允许平衡质量和 token 消耗。
## 配置定义
| 代理 | `quality` | `balanced` | `budget` |
|-------|-----------|------------|----------|
| gsd-planner | opus | opus | sonnet |
| gsd-roadmapper | opus | sonnet | sonnet |
| gsd-executor | opus | sonnet | sonnet |
| gsd-phase-researcher | opus | sonnet | haiku |
| gsd-project-researcher | opus | sonnet | haiku |
| gsd-research-synthesizer | sonnet | sonnet | haiku |
| gsd-debugger | opus | sonnet | sonnet |
| gsd-codebase-mapper | sonnet | haiku | haiku |
| gsd-verifier | sonnet | sonnet | haiku |
| gsd-plan-checker | sonnet | sonnet | haiku |
| gsd-integration-checker | sonnet | sonnet | haiku |
| gsd-nyquist-auditor | sonnet | sonnet | haiku |
## 配置理念
**quality** - 最大推理能力
- 所有决策代理使用 Opus
- 只读验证使用 Sonnet
- 适用场景:有配额可用、关键架构工作
**balanced**(默认)- 智能分配
- 仅规划(架构决策发生的地方)使用 Opus
- 执行和研究使用 Sonnet(遵循明确指令)
- 验证使用 Sonnet(需要推理,不仅仅是模式匹配)
- 适用场景:正常开发、质量与成本的良好平衡
**budget** - 最小化 Opus 使用
- 编写代码的使用 Sonnet
- 研究和验证使用 Haiku
- 适用场景:节省配额、大量工作、不太关键的阶段
## 解析逻辑
编排器在生成代理前解析模型:
```
1. 读取 .planning/config.json
2. 检查 model_overrides 是否有代理特定覆盖
3. 如果没有覆盖,在配置表中查找代理
4. 将 model 参数传递给 Task 调用
```
## 单代理覆盖
覆盖特定代理而不更改整个配置:
```json
{
"model_profile": "balanced",
"model_overrides": {
"gsd-executor": "opus",
"gsd-planner": "haiku"
}
}
```
覆盖优先于配置。有效值:`opus`、`sonnet`、`haiku`。
## 切换配置
运行时:`/gsd:set-profile <profile>`
项目默认值:在 `.planning/config.json` 中设置:
```json
{
"model_profile": "balanced"
}
```
## 设计理由
**为什么 gsd-planner 使用 Opus?**
规划涉及架构决策、目标分解和任务设计。这是模型质量影响最大的地方。
**为什么 gsd-executor 使用 Sonnet?**
执行者遵循明确的 PLAN.md 指令。计划已包含推理;执行只是实现。
**为什么 balanced 中验证器使用 Sonnet(而非 Haiku)?**
验证需要目标回溯推理 —— 检查代码是否**交付**了阶段承诺的内容,而不仅仅是模式匹配。Sonnet 处理得很好;Haiku 可能会遗漏细微的差距。
**为什么 gsd-codebase-mapper 使用 Haiku?**
只读探索和模式提取。不需要推理,只需从文件内容输出结构化结果。
**为什么用 `inherit` 而不是直接传递 `opus`?**
Claude Code 的 `"opus"` 别名映射到特定模型版本。组织可能阻止旧版 opus 而允许新版。GSD 为 opus 级代理返回 `"inherit"`,使其使用用户在会话中配置的任何 opus 版本。这避免了版本冲突和静默回退到 Sonnet。

View File

@@ -0,0 +1,61 @@
# 阶段参数解析
为操作阶段的命令解析和规范化阶段参数。
## 提取
从 `$ARGUMENTS` 中:
- 提取阶段编号(第一个数字参数)
- 提取标志(以 `--` 为前缀)
- 剩余文本为描述(用于 insert/add 命令)
## 使用 gsd-tools
`find-phase` 命令一步完成规范化和验证:
```bash
PHASE_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}")
```
返回 JSON 包含:
- `found`: true/false
- `directory`: 阶段目录的完整路径
- `phase_number`: 规范化的编号(如 "06"、"06.1")
- `phase_name`: 名称部分(如 "foundation")
- `plans`: PLAN.md 文件数组
- `summaries`: SUMMARY.md 文件数组
## 手动规范化(遗留)
将整数阶段补零到 2 位。保留小数后缀。
```bash
# 规范化阶段编号
if [[ "$PHASE" =~ ^[0-9]+$ ]]; then
# 整数: 8 → 08
PHASE=$(printf "%02d" "$PHASE")
elif [[ "$PHASE" =~ ^([0-9]+)\.([0-9]+)$ ]]; then
# 小数: 2.1 → 02.1
PHASE=$(printf "%02d.%s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}")
fi
```
## 验证
使用 `roadmap get-phase` 验证阶段存在:
```bash
PHASE_CHECK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}")
if [ "$(printf '%s\n' "$PHASE_CHECK" | jq -r '.found')" = "false" ]; then
echo "ERROR: Phase ${PHASE} not found in roadmap"
exit 1
fi
```
## 目录查找
使用 `find-phase` 进行目录查找:
```bash
PHASE_DIR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}" --raw)
```

View File

@@ -0,0 +1,200 @@
<planning_config>
`.planning/` 目录行为的配置选项。
<config_schema>
```json
"planning": {
"commit_docs": true,
"search_gitignored": false
},
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}"
}
```
| 选项 | 默认值 | 描述 |
|--------|---------|-------------|
| `commit_docs` | `true` | 是否将规划工件提交到 git |
| `search_gitignored` | `false` | 在广泛 rg 搜索中添加 `--no-ignore` |
| `git.branching_strategy` | `"none"` | Git 分支策略:`"none"`、`"phase"` 或 `"milestone"` |
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | 阶段策略的分支模板 |
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | 里程碑策略的分支模板 |
</config_schema>
<commit_docs_behavior>
**当 `commit_docs: true`(默认):**
- 规划文件正常提交
- SUMMARY.md、STATE.md、ROADMAP.md 在 git 中跟踪
- 规划决策的完整历史保留
**当 `commit_docs: false`:**
- 跳过 `.planning/` 文件的所有 `git add`/`git commit`
- 用户必须将 `.planning/` 添加到 `.gitignore`
- 适用于:OSS 贡献、客户项目、保持规划私有
**使用 gsd-tools.cjs(推荐):**
```bash
# 提交时自动检查 commit_docs + gitignore:
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md
# 通过 state load 加载配置(返回 JSON):
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# commit_docs 在 JSON 输出中可用
# 或使用包含 commit_docs 的 init 命令:
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# commit_docs 包含在所有 init 命令输出中
```
**自动检测:** 如果 `.planning/` 被 gitignore,无论 config.json 如何,`commit_docs` 自动为 `false`。这防止用户在 `.gitignore` 中有 `.planning/` 时出现 git 错误。
**通过 CLI 提交(自动处理检查):**
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md
```
CLI 在内部检查 `commit_docs` 配置和 gitignore 状态 —— 无需手动条件判断。
</commit_docs_behavior>
<search_behavior>
**当 `search_gitignored: false`(默认):**
- 标准 rg 行为(尊重 .gitignore)
- 直接路径搜索有效:`rg "pattern" .planning/` 找到文件
- 广泛搜索跳过 gitignored:`rg "pattern"` 跳过 `.planning/`
**当 `search_gitignored: true`:**
- 在应该包含 `.planning/` 的广泛 rg 搜索中添加 `--no-ignore`
- 仅在搜索整个仓库并期望 `.planning/` 匹配时需要
**注意:** 大多数 GSD 操作使用直接文件读取或显式路径,无论 gitignore 状态如何都有效。
</search_behavior>
<setup_uncommitted_mode>
使用未提交模式:
1. **设置配置:**
```json
"planning": {
"commit_docs": false,
"search_gitignored": true
}
```
2. **添加到 .gitignore:**
```
.planning/
```
3. **已存在的跟踪文件:** 如果 `.planning/` 之前被跟踪:
```bash
git rm -r --cached .planning/
git commit -m "chore: stop tracking planning docs"
```
4. **分支合并:** 当使用 `branching_strategy: phase` 或 `milestone` 时,`complete-milestone` 工作流在 `commit_docs: false` 时自动从暂存区移除 `.planning/` 文件,然后才进行合并提交。
</setup_uncommitted_mode>
<branching_strategy_behavior>
**分支策略:**
| 策略 | 创建分支时机 | 分支范围 | 合并点 |
|----------|---------------------|--------------|-------------|
| `none` | 从不 | N/A | N/A |
| `phase` | `execute-phase` 开始时 | 单个阶段 | 阶段后用户手动合并 |
| `milestone` | 里程碑第一个 `execute-phase` | 整个里程碑 | `complete-milestone` 时 |
**当 `git.branching_strategy: "none"`(默认):**
- 所有工作提交到当前分支
- 标准 GSD 行为
**当 `git.branching_strategy: "phase"`:**
- `execute-phase` 在执行前创建/切换到分支
- 分支名来自 `phase_branch_template`(如 `gsd/phase-03-authentication`)
- 所有计划提交到该分支
- 阶段完成后用户手动合并分支
- `complete-milestone` 提供合并所有阶段分支的选项
**当 `git.branching_strategy: "milestone"`:**
- 里程碑的第一个 `execute-phase` 创建里程碑分支
- 分支名来自 `milestone_branch_template`(如 `gsd/v1.0-mvp`)
- 里程碑中所有阶段提交到同一分支
- `complete-milestone` 提供将里程碑分支合并到 main 的选项
**模板变量:**
| 变量 | 可用于 | 描述 |
|----------|--------------|-------------|
| `{phase}` | phase_branch_template | 零填充阶段号(如 "03") |
| `{slug}` | 两者 | 小写、连字符名称 |
| `{milestone}` | milestone_branch_template | 里程碑版本(如 "v1.0") |
**检查配置:**
使用 `init execute-phase` 返回所有配置为 JSON:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# JSON 输出包含:branching_strategy, phase_branch_template, milestone_branch_template
```
或使用 `state load` 获取配置值:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# 从 JSON 解析 branching_strategy, phase_branch_template, milestone_branch_template
```
**分支创建:**
```bash
# 阶段策略
if [ "$BRANCHING_STRATEGY" = "phase" ]; then
PHASE_SLUG=$(echo "$PHASE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
BRANCH_NAME=$(echo "$PHASE_BRANCH_TEMPLATE" | sed "s/{phase}/$PADDED_PHASE/g" | sed "s/{slug}/$PHASE_SLUG/g")
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
fi
# 里程碑策略
if [ "$BRANCHING_STRATEGY" = "milestone" ]; then
MILESTONE_SLUG=$(echo "$MILESTONE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
BRANCH_NAME=$(echo "$MILESTONE_BRANCH_TEMPLATE" | sed "s/{milestone}/$MILESTONE_VERSION/g" | sed "s/{slug}/$MILESTONE_SLUG/g")
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
fi
```
**complete-milestone 时的合并选项:**
| 选项 | Git 命令 | 结果 |
|--------|-------------|--------|
| Squash 合并(推荐) | `git merge --squash` | 每个分支单个干净提交 |
| 带历史合并 | `git merge --no-ff` | 保留所有单独提交 |
| 不合并直接删除 | `git branch -D` | 丢弃分支工作 |
| 保留分支 | (无) | 后续手动处理 |
推荐 Squash 合并 —— 保持 main 分支历史干净,同时在分支中保留完整开发历史(直到删除)。
**使用场景:**
| 策略 | 最适合 |
|----------|----------|
| `none` | 独立开发、简单项目 |
| `phase` | 每阶段代码审查、细粒度回滚、团队协作 |
| `milestone` | 发布分支、预发布环境、每个版本一个 PR |
</branching_strategy_behavior>
</planning_config>

View File

@@ -0,0 +1,142 @@
# 提问指南
项目初始化是梦想提取,而非需求收集。你在帮助用户发现和表达他们想构建的内容。这不是合同谈判 —— 是协作思考。
## 理念
**你是思考伙伴,不是面试官。**
用户通常有一个模糊的想法。你的工作是帮助他们将其锐化。问一些让他们思考"哦,我没想到那个"或"是的,这正是我的意思"的问题。
不要审问。协作。不要照本宣科。顺藤摸瓜。
## 目标
到提问结束时,你需要足够的清晰度来编写下游阶段可执行的 PROJECT.md:
- **研究** 需要:研究什么领域、用户已知什么、存在哪些未知
- **需求** 需要:足够清晰的愿景来界定 v1 功能
- **路线图** 需要:足够清晰的愿景来分解为阶段、"完成"是什么样子
- **plan-phase** 需要:可分解为任务的具体需求、实现选择的上下文
- **execute-phase** 需要:可验证的成功标准、需求背后的"为什么"
模糊的 PROJECT.md 会让每个下游阶段都在猜测。成本会叠加。
## 如何提问
**开放开始。** 让他们倾倒心理模型。不要用结构打断。
**跟随能量。** 无论他们强调什么,深入那个。什么让他们兴奋?什么问题引发了这一切?
**挑战模糊。** 绝不接受模糊回答。"好"意味着什么?"用户"指谁?"简单"是怎么简单?
**让抽象具体。**"带我走一遍使用这个。""那实际看起来是什么样?"
**澄清歧义。**"你说 Z 时,是指 A 还是 B?""你提到了 X —— 跟我多说说。"
**知道何时停止。** 当你理解他们想要什么、为什么想要、给谁用、完成是什么样 —— 提议继续。
## 问题类型
以此作为灵感,不是清单。选择与话题相关的。
**动机 —— 为什么存在:**
- "什么引发了这一切?"
- "你今天在做什么会被这个替代?"
- "如果这个存在,你会做什么?"
**具体性 —— 它实际是什么:**
- "带我走一遍使用这个"
- "你说 X —— 那实际看起来是什么样?"
- "给我一个例子"
**澄清 —— 他们什么意思:**
- "你说 Z 时,是指 A 还是 B?"
- "你提到了 X —— 跟我多说说那个"
**成功 —— 你怎么知道它在工作:**
- "你怎么知道这个在工作?"
- "完成是什么样子?"
## 使用 AskUserQuestion
用 AskUserQuestion 帮助用户思考,通过呈现具体的选项供他们反应。
**好选项:**
- 他们可能意思的解读
- 确认或否认的具体例子
- 揭示优先级的具体选择
**坏选项:**
- 泛泛的类别("技术"、"业务"、"其他")
- 预设答案的引导性选项
- 选项太多(2-4 个理想)
- 超过 12 个字符的标题(硬限制 —— 验证会拒绝)
**示例 —— 模糊回答:**
用户说"它应该快"
- header: "快"
- question: "快是指?"
- options: ["亚秒响应", "处理大数据集", "快速构建", "让我解释"]
**示例 —— 跟随话题:**
用户提到"对当前工具感到沮丧"
- header: "沮丧"
- question: "具体什么让你沮丧?"
- options: ["点击太多", "缺少功能", "不可靠", "让我解释"]
**给用户的提示 —— 修改选项:**
想要稍微修改某个选项版本的用户可以选择"Other"并通过编号引用选项:`#1 但仅用于指关节` 或 `#2 禁用分页`。这避免重新输入完整选项文本。
## 自由格式规则
**当用户想自由解释时,停止使用 AskUserQuestion。**
如果用户选择"Other"且他们的回应表明他们想用自己的话描述(如"让我描述一下"、"我来解释"、"别的"、或任何非选择/修改现有选项的开放式回复),你必须:
1. **用纯文本问你的追问** — 不通过 AskUserQuestion
2. **等待他们在正常提示符下输入**
3. **仅在处理他们的自由格式回应后恢复 AskUserQuestion**
同样适用于如果你包含一个表明自由格式的选项(如"让我解释"或"详细描述")且用户选择了它。
**错误:** 用户说"让我描述一下" → AskUserQuestion("什么功能?", ["功能 A", "功能 B", "详细描述"])
**正确:** 用户说"让我描述一下" → "请讲 —— 你在想什么?"
## 上下文清单
以此作为**背景清单**,而非对话结构。进行时在脑中检查这些。如果还有缺口,自然地穿插问题。
- [ ] 他们在构建什么(足够具体可以向陌生人解释)
- [ ] 为什么它需要存在(驱动它的问题或渴望)
- [ ] 给谁用的(即使只是他们自己)
- [ ] "完成"是什么样子(可观察的结果)
四件事。如果他们主动提供更多,捕获它。
## 决策门控
当你能写出清晰的 PROJECT.md 时,提议继续:
- header: "准备好了?"
- question: "我想我理解你想要什么了。准备创建 PROJECT.md 吗?"
- options:
- "创建 PROJECT.md" — 让我们继续
- "继续探索" — 我想分享更多 / 再问我
如果"继续探索" —— 问他们想添加什么或识别缺口并自然探查。
循环直到选择"创建 PROJECT.md"。
## 反模式
- **走清单** — 不管他们说什么都按领域走
- **套话问题** — "你的核心价值是什么?""什么超出范围?"不管上下文
- **企业腔** — "你的成功标准是什么?""你的利益相关者是谁?"
- **审问** — 不基于回答构建就连续发问
- **急于求成** — 最小化问题以开始"实际工作"
- **浅层接受** — 不探查就接受模糊回答
- **过早约束** — 还不理解想法就问技术栈
- **用户技能** — 绝不问用户的技术经验。Claude 来构建。

View File

@@ -0,0 +1,263 @@
<overview>
TDD 关乎设计质量,而非覆盖率指标。红-绿-重构循环迫使你在实现前思考行为,从而产生更清晰的接口和更可测试的代码。
**原则:** 如果在编写 `fn` 之前能用 `expect(fn(input)).toBe(output)` 描述行为,TDD 会改善结果。
**关键洞察:** TDD 工作本质上比标准任务更重 —— 它需要 2-3 个执行周期(RED → GREEN → REFACTOR),每个周期都涉及文件读取、测试运行和可能的调试。TDD 功能获得专门的计划,以确保整个周期内有完整的上下文可用。
</overview>
<when_to_use_tdd>
## 何时 TDD 提高质量
**TDD 候选(创建 TDD 计划):**
- 有明确输入/输出的业务逻辑
- 有请求/响应契约的 API 端点
- 数据转换、解析、格式化
- 验证规则和约束
- 有可测试行为的算法
- 状态机和工作流
- 有清晰规格的工具函数
**跳过 TDD(使用带 `type="auto"` 任务的标准计划):**
- UI 布局、样式、视觉组件
- 配置更改
- 连接现有组件的胶水代码
- 一次性脚本和迁移
- 无业务逻辑的简单 CRUD
- 探索性原型
**启发式:** 能在编写 `fn` 之前写 `expect(fn(input)).toBe(output)` 吗?
→ 能:创建 TDD 计划
→ 不能:使用标准计划,事后添加测试(如需要)
</when_to_use_tdd>
<tdd_plan_structure>
## TDD 计划结构
每个 TDD 计划通过完整的 RED-GREEN-REFACTOR 循环实现**一个功能**。
```markdown
---
phase: XX-name
plan: NN
type: tdd
---
<objective>
[什么功能以及为什么]
Purpose: [该功能 TDD 的设计收益]
Output: [可工作的、已测试的功能]
</objective>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@relevant/source/files.ts
</context>
<feature>
<name>[功能名称]</name>
<files>[源文件, 测试文件]</files>
<behavior>
[可测试术语描述的预期行为]
Cases: 输入 → 预期输出
</behavior>
<implementation>[测试通过后如何实现]</implementation>
</feature>
<verification>
[证明功能有效的测试命令]
</verification>
<success_criteria>
- 失败测试已编写并提交
- 实现通过测试
- 重构完成(如需要)
- 所有 2-3 个提交都存在
</success_criteria>
<output>
完成后,创建包含以下内容的 SUMMARY.md:
- RED: 编写了什么测试,为什么失败
- GREEN: 什么实现让它通过
- REFACTOR: 做了什么清理(如有)
- Commits: 生成的提交列表
</output>
```
**每个 TDD 计划一个功能。** 如果功能足够简单可以批量处理,那就足够简单可以跳过 TDD —— 使用标准计划,事后添加测试。
</tdd_plan_structure>
<execution_flow>
## 红-绿-重构循环
**RED - 编写失败测试:**
1. 按项目约定创建测试文件
2. 编写描述预期行为的测试(来自 `<behavior>` 元素)
3. 运行测试 - 必须**失败**
4. 如果测试通过:功能已存在或测试有误。调查。
5. 提交:`test({phase}-{plan}): add failing test for [feature]`
**GREEN - 实现使其通过:**
1. 编写使测试通过的最小代码
2. 不耍小聪明,不优化 - 只让它工作
3. 运行测试 - 必须**通过**
4. 提交:`feat({phase}-{plan}): implement [feature]`
**REFACTOR(如需要):**
1. 如果存在明显的改进,清理实现
2. 运行测试 - 必须**仍然通过**
3. 仅在做出更改时提交:`refactor({phase}-{plan}): clean up [feature]`
**结果:** 每个 TDD 计划产生 2-3 个原子提交。
</execution_flow>
<test_quality>
## 好测试 vs 坏测试
**测试行为,而非实现:**
- 好:"返回格式化的日期字符串"
- 坏:"用正确参数调用 formatDate 辅助函数"
- 测试应该能经受重构
**每个测试一个概念:**
- 好:分别为有效输入、空输入、畸形输入编写测试
- 坏:用多个断言检查所有边缘情况的单个测试
**描述性名称:**
- 好:"should reject empty email"、"returns null for invalid ID"
- 坏:"test1"、"handles error"、"works correctly"
**不包含实现细节:**
- 好:测试公共 API、可观察行为
- 坏:Mock 内部实现、测试私有方法、断言内部状态
</test_quality>
<framework_setup>
## 测试框架设置(如不存在)
当执行 TDD 计划但没有配置测试框架时,作为 RED 阶段的一部分进行设置:
**1. 检测项目类型:**
```bash
# JavaScript/TypeScript
if [ -f package.json ]; then echo "node"; fi
# Python
if [ -f requirements.txt ] || [ -f pyproject.toml ]; then echo "python"; fi
# Go
if [ -f go.mod ]; then echo "go"; fi
# Rust
if [ -f Cargo.toml ]; then echo "rust"; fi
```
**2. 安装最小框架:**
| 项目 | 框架 | 安装 |
|---------|-----------|---------|
| Node.js | Jest | `npm install -D jest @types/jest ts-jest` |
| Node.js (Vite) | Vitest | `npm install -D vitest` |
| Python | pytest | `pip install pytest` |
| Go | testing | 内置 |
| Rust | cargo test | 内置 |
**3. 按需创建配置:**
- Jest: 带 ts-jest preset 的 `jest.config.js`
- Vitest: 带测试全局变量的 `vitest.config.ts`
- pytest: `pytest.ini` 或 `pyproject.toml` 部分
**4. 验证设置:**
```bash
# 运行空测试套件 - 应该以 0 个测试通过
npm test # Node
pytest # Python
go test ./... # Go
cargo test # Rust
```
**5. 创建第一个测试文件:**
遵循项目约定的测试位置:
- 源文件旁边的 `*.test.ts` / `*.spec.ts`
- `__tests__/` 目录
- 根目录的 `tests/` 目录
框架设置是第一个 TDD 计划 RED 阶段的一次性成本。
</framework_setup>
<error_handling>
## 错误处理
**测试在 RED 阶段没有失败:**
- 功能可能已存在 - 调查
- 测试可能有误(没测试你以为的东西)
- 前进前修复
**测试在 GREEN 阶段没有通过:**
- 调试实现
- 不要跳到重构
- 持续迭代直到绿色
**测试在 REFACTOR 阶段失败:**
- 撤销重构
- 提交过早
- 用更小的步骤重构
**不相关的测试失败:**
- 停下来调查
- 可能表明耦合问题
- 前进前修复
</error_handling>
<commit_pattern>
## TDD 计划的提交模式
TDD 计划产生 2-3 个原子提交(每个阶段一个):
```
test(08-02): add failing test for email validation
- Tests valid email formats accepted
- Tests invalid formats rejected
- Tests empty input handling
feat(08-02): implement email validation
- Regex pattern matches RFC 5322
- Returns boolean for validity
- Handles edge cases (empty, null)
refactor(08-02): extract regex to constant (optional)
- Moved pattern to EMAIL_REGEX constant
- No behavior changes
- Tests still pass
```
**与标准计划对比:**
- 标准计划:每个任务 1 个提交,每个计划 2-4 个提交
- TDD 计划:单个功能 2-3 个提交
两者遵循相同格式:`{type}({phase}-{plan}): {description}`
**好处:**
- 每个提交独立可回滚
- Git bisect 在提交级别工作
- 显示 TDD 纪律的清晰历史
- 与整体提交策略一致
</commit_pattern>
<context_budget>
## 上下文预算
TDD 计划目标 **~40% 上下文使用率**(低于标准计划的 ~50%)。
为什么更低:
- RED 阶段:编写测试、运行测试、可能调试为什么没有失败
- GREEN 阶段:实现、运行测试、可能对失败进行迭代
- REFACTOR 阶段:修改代码、运行测试、验证无回归
每个阶段涉及读取文件、运行命令、分析输出。来回往复本质上比线性任务执行更重。
单一功能聚焦确保整个周期保持完整质量。
</context_budget>

View File

@@ -0,0 +1,158 @@
# UI 品牌规范
面向用户的 GSD 输出的视觉模式。编排器通过 @ 引用此文件。
## 阶段横幅
用于主要工作流过渡。
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► {阶段名称}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**阶段名称(大写):**
- `QUESTIONING`(提问)
- `RESEARCHING`(研究)
- `DEFINING REQUIREMENTS`(定义需求)
- `CREATING ROADMAP`(创建路线图)
- `PLANNING PHASE {N}`(规划阶段 {N})
- `EXECUTING WAVE {N}`(执行波次 {N})
- `VERIFYING`(验证)
- `PHASE {N} COMPLETE ✓`(阶段 {N} 完成)
- `MILESTONE COMPLETE 🎉`(里程碑完成)
---
## 检查点框
需要用户操作。62 字符宽度。
```
╔══════════════════════════════════════════════════════════════╗
║ CHECKPOINT: {类型} ║
╚══════════════════════════════════════════════════════════════╝
{内容}
──────────────────────────────────────────────────────────────
→ {操作提示}
──────────────────────────────────────────────────────────────
```
**类型:**
- `CHECKPOINT: 需要验证` → `→ 输入 "approved" 或描述问题`
- `CHECKPOINT: 需要决策` → `→ 选择: option-a / option-b`
- `CHECKPOINT: 需要操作` → `→ 完成后输入 "done"`
---
## 状态符号
```
✓ 完成 / 通过 / 已验证
✗ 失败 / 缺失 / 阻塞
◆ 进行中
○ 待处理
⚡ 自动批准
⚠ 警告
🎉 里程碑完成(仅在横幅中)
```
---
## 进度显示
**阶段/里程碑级别:**
```
进度: ████████░░ 80%
```
**任务级别:**
```
任务: 2/4 完成
```
**计划级别:**
```
计划: 3/5 完成
```
---
## 生成指示器
```
◆ 正在生成研究员...
◆ 并行生成 4 个研究员...
→ 技术栈研究
→ 功能研究
→ 架构研究
→ 陷阱研究
✓ 研究员完成: STACK.md 已写入
```
---
## 下一步区块
始终在主要完成后。
```
───────────────────────────────────────────────────────────────
## ▶ 下一步
**{标识符}: {名称}** — {单行描述}
`{可复制粘贴的命令}`
<sub>`/clear` 优先 → 全新上下文窗口</sub>
───────────────────────────────────────────────────────────────
**也可选:**
- `/gsd:alternative-1` — 描述
- `/gsd:alternative-2` — 描述
───────────────────────────────────────────────────────────────
```
---
## 错误框
```
╔══════════════════════════════════════════════════════════════╗
║ ERROR ║
╚══════════════════════════════════════════════════════════════╝
{错误描述}
**修复方法:** {解决步骤}
```
---
## 表格
```
| 阶段 | 状态 | 计划 | 进度 |
|------|------|------|------|
| 1 | ✓ | 3/3 | 100% |
| 2 | ◆ | 1/4 | 25% |
| 3 | ○ | 0/2 | 0% |
```
---
## 反模式
- 变化的框/横幅宽度
- 混合横幅样式(`===`、`---`、`***`)
- 横幅中缺少 `GSD ►` 前缀
- 随机 emoji(`🚀`、`✨`、`💫`)
- 完成后缺少下一步区块

View File

@@ -0,0 +1,612 @@
# 验证模式
如何验证不同类型的工件是真实实现,而非存根或占位符。
<core_principle>
**存在 ≠ 实现**
文件存在并不意味着功能有效。验证必须检查:
1. **存在** - 文件在预期路径
2. **实质性** - 内容是真实实现,非占位符
3. **已连接** - 已连接到系统的其他部分
4. **功能性** - 调用时实际工作
级别 1-3 可以编程检查。级别 4 通常需要人工验证。
</core_principle>
<stub_detection>
## 通用存根模式
这些模式表明占位符代码,无论文件类型:
**基于注释的存根:**
```bash
# 存根注释的 Grep 模式
grep -E "(TODO|FIXME|XXX|HACK|PLACEHOLDER)" "$file"
grep -E "implement|add later|coming soon|will be" "$file" -i
grep -E "// \.\.\.|/\* \.\.\. \*/|# \.\.\." "$file"
```
**输出中的占位符文本:**
```bash
# UI 占位符模式
grep -E "placeholder|lorem ipsum|coming soon|under construction" "$file" -i
grep -E "sample|example|test data|dummy" "$file" -i
grep -E "\[.*\]|<.*>|\{.*\}" "$file" # 模板括号未移除
```
**空或琐碎实现:**
```bash
# 什么都不做的函数
grep -E "return null|return undefined|return \{\}|return \[\]" "$file"
grep -E "pass$|\.\.\.|\bnothing\b" "$file"
grep -E "console\.(log|warn|error).*only" "$file" # 仅日志函数
```
**预期动态但硬编码的值:**
```bash
# 硬编码 ID、计数或内容
grep -E "id.*=.*['\"].*['\"]" "$file" # 硬编码字符串 ID
grep -E "count.*=.*\d+|length.*=.*\d+" "$file" # 硬编码计数
grep -E "\\\$\d+\.\d{2}|\d+ items" "$file" # 硬编码显示值
```
</stub_detection>
<react_components>
## React/Next.js 组件
**存在检查:**
```bash
# 文件存在且导出组件
[ -f "$component_path" ] && grep -E "export (default |)function|export const.*=.*\(" "$component_path"
```
**实质性检查:**
```bash
# 返回实际 JSX,非占位符
grep -E "return.*<" "$component_path" | grep -v "return.*null" | grep -v "placeholder" -i
# 有有意义的内容(不仅仅是包装 div)
grep -E "<[A-Z][a-zA-Z]+|className=|onClick=|onChange=" "$component_path"
# 使用 props 或 state(非静态)
grep -E "props\.|useState|useEffect|useContext|\{.*\}" "$component_path"
```
**React 特有的存根模式:**
```javascript
// 危险信号 - 这些是存根:
return <div>Component</div>
return <div>Placeholder</div>
return <div>{/* TODO */}</div>
return <p>Coming soon</p>
return null
return <></>
// 也是存根 - 空处理器:
onClick={() => {}}
onChange={() => console.log('clicked')}
onSubmit={(e) => e.preventDefault()} // 仅阻止默认,什么都不做
```
**连接检查:**
```bash
# 组件导入它需要的东西
grep -E "^import.*from" "$component_path"
# Props 实际被使用(不仅仅是接收)
# 查找解构或 props.X 用法
grep -E "\{ .* \}.*props|\bprops\.[a-zA-Z]+" "$component_path"
# API 调用存在(对于数据获取组件)
grep -E "fetch\(|axios\.|useSWR|useQuery|getServerSideProps|getStaticProps" "$component_path"
```
**功能验证(需要人工):**
- 组件是否渲染可见内容?
- 交互元素是否响应点击?
- 数据是否加载并显示?
- 错误状态是否适当显示?
</react_components>
<api_routes>
## API 路由(Next.js App Router / Express 等)
**存在检查:**
```bash
# 路由文件存在
[ -f "$route_path" ]
# 导出 HTTP 方法处理器(Next.js App Router)
grep -E "export (async )?(function|const) (GET|POST|PUT|PATCH|DELETE)" "$route_path"
# 或 Express 风格处理器
grep -E "\.(get|post|put|patch|delete)\(" "$route_path"
```
**实质性检查:**
```bash
# 有实际逻辑,不仅仅是 return 语句
wc -l "$route_path" # 超过 10-15 行表明真实实现
# 与数据源交互
grep -E "prisma\.|db\.|mongoose\.|sql|query|find|create|update|delete" "$route_path" -i
# 有错误处理
grep -E "try|catch|throw|error|Error" "$route_path"
# 返回有意义的响应
grep -E "Response\.json|res\.json|res\.send|return.*\{" "$route_path" | grep -v "message.*not implemented" -i
```
**API 路由特有的存根模式:**
```typescript
// 危险信号 - 这些是存根:
export async function POST() {
return Response.json({ message: "Not implemented" })
}
export async function GET() {
return Response.json([]) // 空 array 无数据库查询
}
export async function PUT() {
return new Response() // 空响应
}
// 仅控制台日志:
export async function POST(req) {
console.log(await req.json())
return Response.json({ ok: true })
}
```
**连接检查:**
```bash
# 导入数据库/服务客户端
grep -E "^import.*prisma|^import.*db|^import.*client" "$route_path"
# 实际使用请求体(对于 POST/PUT)
grep -E "req\.json\(\)|req\.body|request\.json\(\)" "$route_path"
# 验证输入(不仅仅信任请求)
grep -E "schema\.parse|validate|zod|yup|joi" "$route_path"
```
**功能验证(人工或自动化):**
- GET 是否从数据库返回真实数据?
- POST 是否实际创建记录?
- 错误响应是否有正确的状态码?
- 认证检查是否实际执行?
</api_routes>
<database_schema>
## 数据库模式(Prisma / Drizzle / SQL)
**存在检查:**
```bash
# 模式文件存在
[ -f "prisma/schema.prisma" ] || [ -f "drizzle/schema.ts" ] || [ -f "src/db/schema.sql" ]
# 模型/表已定义
grep -E "^model $model_name|CREATE TABLE $table_name|export const $table_name" "$schema_path"
```
**实质性检查:**
```bash
# 有预期字段(不仅仅是 id)
grep -A 20 "model $model_name" "$schema_path" | grep -E "^\s+\w+\s+\w+"
# 有预期关系
grep -E "@relation|REFERENCES|FOREIGN KEY" "$schema_path"
# 有适当的字段类型(不全是 String)
grep -A 20 "model $model_name" "$schema_path" | grep -E "Int|DateTime|Boolean|Float|Decimal|Json"
```
**模式特有的存根模式:**
```prisma
// 危险信号 - 这些是存根:
model User {
id String @id
// TODO: add fields
}
model Message {
id String @id
content String // 只有一个真实字段
}
// 缺少关键字段:
model Order {
id String @id
// 缺少: userId, items, total, status, createdAt
}
```
**连接检查:**
```bash
# 迁移存在且已应用
ls prisma/migrations/ 2>/dev/null | wc -l # 应该 > 0
npx prisma migrate status 2>/dev/null | grep -v "pending"
# 客户端已生成
[ -d "node_modules/.prisma/client" ]
```
**功能验证:**
```bash
# 可以查询表(自动化)
npx prisma db execute --stdin <<< "SELECT COUNT(*) FROM $table_name"
```
</database_schema>
<hooks_utilities>
## 自定义 Hooks 和工具
**存在检查:**
```bash
# 文件存在且导出函数
[ -f "$hook_path" ] && grep -E "export (default )?(function|const)" "$hook_path"
```
**实质性检查:**
```bash
# Hook 使用 React hooks(对于自定义 hooks)
grep -E "useState|useEffect|useCallback|useMemo|useRef|useContext" "$hook_path"
# 有有意义的返回值
grep -E "return \{|return \[" "$hook_path"
# 超过琐碎长度
[ $(wc -l < "$hook_path") -gt 10 ]
```
**Hooks 特有的存根模式:**
```typescript
// 危险信号 - 这些是存根:
export function useAuth() {
return { user: null, login: () => {}, logout: () => {} }
}
export function useCart() {
const [items, setItems] = useState([])
return { items, addItem: () => console.log('add'), removeItem: () => {} }
}
// 硬编码返回:
export function useUser() {
return { name: "Test User", email: "test@example.com" }
}
```
**连接检查:**
```bash
# Hook 实际在某处被导入
grep -r "import.*$hook_name" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
# Hook 实际被调用
grep -r "$hook_name()" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
```
</hooks_utilities>
<environment_config>
## 环境变量和配置
**存在检查:**
```bash
# .env 文件存在
[ -f ".env" ] || [ -f ".env.local" ]
# 必需变量已定义
grep -E "^$VAR_NAME=" .env .env.local 2>/dev/null
```
**实质性检查:**
```bash
# 变量有实际值(非占位符)
grep -E "^$VAR_NAME=.+" .env .env.local 2>/dev/null | grep -v "your-.*-here|xxx|placeholder|TODO" -i
# 值对类型看起来有效:
# - URL 应以 http 开头
# - 密钥应足够长
# - 布尔值应为 true/false
```
**环境变量特有的存根模式:**
```bash
# 危险信号 - 这些是存根:
DATABASE_URL=your-database-url-here
STRIPE_SECRET_KEY=sk_test_xxx
API_KEY=placeholder
NEXT_PUBLIC_API_URL=http://localhost:3000 # 生产环境仍指向 localhost
```
**连接检查:**
```bash
# 变量实际在代码中使用
grep -r "process\.env\.$VAR_NAME|env\.$VAR_NAME" src/ --include="*.ts" --include="*.tsx"
# 变量在验证模式中(如果使用 zod 等验证 env)
grep -E "$VAR_NAME" src/env.ts src/env.mjs 2>/dev/null
```
</environment_config>
<wiring_verification>
## 连接验证模式
连接验证检查组件是否实际通信。这是大多数存根隐藏的地方。
### 模式:组件 → API
**检查:** 组件是否实际调用 API?
```bash
# 查找 fetch/axios 调用
grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component_path"
# 验证未被注释掉
grep -E "fetch\(|axios\." "$component_path" | grep -v "^.*//.*fetch"
# 检查响应被使用
grep -E "await.*fetch|\.then\(|setData|setState" "$component_path"
```
**危险信号:**
```typescript
// Fetch 存在但响应被忽略:
fetch('/api/messages') // 无 await,无 .then,无赋值
// Fetch 在注释中:
// fetch('/api/messages').then(r => r.json()).then(setMessages)
// Fetch 到错误的端点:
fetch('/api/message') // 拼写错误 - 应该是 /api/messages
```
### 模式:API → 数据库
**检查:** API 路由是否实际查询数据库?
```bash
# 查找数据库调用
grep -E "prisma\.$model|db\.query|Model\.find" "$route_path"
# 验证被 await
grep -E "await.*prisma|await.*db\." "$route_path"
# 检查结果被返回
grep -E "return.*json.*data|res\.json.*result" "$route_path"
```
**危险信号:**
```typescript
// 查询存在但结果未返回:
await prisma.message.findMany()
return Response.json({ ok: true }) // 返回静态值,非查询结果
// 查询未被 await:
const messages = prisma.message.findMany() // 缺少 await
return Response.json(messages) // 返回 Promise,非数据
```
### 模式:表单 → 处理器
**检查:** 表单提交是否实际做些什么?
```bash
# 查找 onSubmit 处理器
grep -E "onSubmit=\{|handleSubmit" "$component_path"
# 检查处理器有内容
grep -A 10 "onSubmit.*=" "$component_path" | grep -E "fetch|axios|mutate|dispatch"
# 验证不仅仅是 preventDefault
grep -A 5 "onSubmit" "$component_path" | grep -v "only.*preventDefault" -i
```
**危险信号:**
```typescript
// 处理器仅阻止默认:
onSubmit={(e) => e.preventDefault()}
// 处理器仅日志:
const handleSubmit = (data) => {
console.log(data)
}
// 处理器为空:
onSubmit={() => {}}
```
### 模式:状态 → 渲染
**检查:** 组件是否渲染状态,而非硬编码内容?
```bash
# 查找 JSX 中的状态使用
grep -E "\{.*messages.*\}|\{.*data.*\}|\{.*items.*\}" "$component_path"
# 检查状态的 map/render
grep -E "\.map\(|\.filter\(|\.reduce\(" "$component_path"
# 验证动态内容
grep -E "\{[a-zA-Z_]+\." "$component_path" # 变量插值
```
**危险信号:**
```tsx
// 硬编码而非状态:
return <div>
<p>Message 1</p>
<p>Message 2</p>
</div>
// 状态存在但未渲染:
const [messages, setMessages] = useState([])
return <div>No messages</div> // 总是显示 "no messages"
// 渲染错误的状态:
const [messages, setMessages] = useState([])
return <div>{otherData.map(...)}</div> // 使用不同数据
```
</wiring_verification>
<verification_checklist>
## 快速验证清单
对于每种工件类型,运行此清单:
### 组件清单
- [ ] 文件存在于预期路径
- [ ] 导出函数/const 组件
- [ ] 返回 JSX(非 null/空)
- [ ] 渲染中无占位符文本
- [ ] 使用 props 或 state(非静态)
- [ ] 事件处理器有真实实现
- [ ] 导入正确解析
- [ ] 在应用某处被使用
### API 路由清单
- [ ] 文件存在于预期路径
- [ ] 导出 HTTP 方法处理器
- [ ] 处理器超过 5 行
- [ ] 查询数据库或服务
- [ ] 返回有意义的响应(非空/占位符)
- [ ] 有错误处理
- [ ] 验证输入
- [ ] 从前端调用
### 模式清单
- [ ] 模型/表已定义
- [ ] 有所有预期字段
- [ ] 字段有适当类型
- [ ] 如需要关系已定义
- [ ] 迁移存在且已应用
- [ ] 客户端已生成
### Hook/工具清单
- [ ] 文件存在于预期路径
- [ ] 导出函数
- [ ] 有有意义的实现(非空返回)
- [ ] 在应用某处被使用
- [ ] 返回值被消费
### 连接清单
- [ ] 组件 → API: fetch/axios 调用存在且使用响应
- [ ] API → 数据库: 查询存在且结果返回
- [ ] 表单 → 处理器: onSubmit 调用 API/mutation
- [ ] 状态 → 渲染: 状态变量出现在 JSX 中
</verification_checklist>
<automated_verification_script>
## 自动化验证方法
对于验证子代理,使用此模式:
```bash
# 1. 检查存在
check_exists() {
[ -f "$1" ] && echo "EXISTS: $1" || echo "MISSING: $1"
}
# 2. 检查存根模式
check_stubs() {
local file="$1"
local stubs=$(grep -c -E "TODO|FIXME|placeholder|not implemented" "$file" 2>/dev/null || echo 0)
[ "$stubs" -gt 0 ] && echo "STUB_PATTERNS: $stubs in $file"
}
# 3. 检查连接(组件调用 API)
check_wiring() {
local component="$1"
local api_path="$2"
grep -q "$api_path" "$component" && echo "WIRED: $component → $api_path" || echo "NOT_WIRED: $component → $api_path"
}
# 4. 检查实质性(超过 N 行,有预期模式)
check_substantive() {
local file="$1"
local min_lines="$2"
local pattern="$3"
local lines=$(wc -l < "$file" 2>/dev/null || echo 0)
local has_pattern=$(grep -c -E "$pattern" "$file" 2>/dev/null || echo 0)
[ "$lines" -ge "$min_lines" ] && [ "$has_pattern" -gt 0 ] && echo "SUBSTANTIVE: $file" || echo "THIN: $file ($lines lines, $has_pattern matches)"
}
```
对每个必须有工件运行这些检查。汇总结果到 VERIFICATION.md。
</automated_verification_script>
<human_verification_triggers>
## 何时需要人工验证
有些事情无法编程验证。标记这些需要人工测试:
**始终人工:**
- 视觉外观(看起来对吗?)
- 用户流程完成(能实际做那件事吗?)
- 实时行为(WebSocket、SSE)
- 外部服务集成(Stripe、邮件发送)
- 错误消息清晰度(消息有帮助吗?)
- 性能感觉(感觉快吗?)
**如不确定则人工:**
- grep 无法追踪的复杂连接
- 依赖状态的动态行为
- 边缘情况和错误状态
- 移动端响应式
- 无障碍性
**人工验证请求格式:**
```markdown
## 需要人工验证
### 1. 聊天消息发送
**测试:** 输入消息并点击发送
**预期:** 消息出现在列表中,输入框清空
**检查:** 刷新后消息是否持久?
### 2. 错误处理
**测试:** 断开网络,尝试发送
**预期:** 错误消息出现,消息未丢失
**检查:** 重连后能重试吗?
```
</human_verification_triggers>
<checkpoint_automation_reference>
## 检查点前自动化
关于自动化优先的检查点模式、服务器生命周期管理、CLI 安装处理和错误恢复协议,请参阅:
**@~/.claude/get-shit-done/references/checkpoints.md** → `<automation_reference>` 部分
关键原则:
- Claude 在呈现检查点**之前**设置验证环境
- 用户从不运行 CLI 命令(仅访问 URL)
- 服务器生命周期:检查点前启动、处理端口冲突、持续运行
- CLI 安装:安全处自动安装,否则检查点让用户选择
- 错误处理:检查点前修复损坏环境,绝不呈现有失败设置的检查点
</checkpoint_automation_reference>

View File

@@ -15,9 +15,12 @@
* state get [section] Get STATE.md content or section
* state patch --field val ... Batch update STATE.md fields
* state begin-phase --phase N --name S --plans C Update STATE.md for new phase start
* state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal
* state signal-resume Remove WAITING.json signal
* resolve-model <agent-type> Get model for agent based on profile
* find-phase <phase> Find phase directory by number
* commit <message> [--files f1 f2] Commit planning docs
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
* commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
* verify-summary <path> Verify a SUMMARY.md file
* generate-slug <text> Convert text to URL-safe slug
* current-timestamp [format] Get timestamp (full|date|filename)
@@ -33,7 +36,7 @@
*
* Phase Operations:
* phase next-decimal <phase> Calculate next decimal phase number
* phase add <description> Append new phase to roadmap + create dir
* phase add <description> [--id ID] Append new phase to roadmap + create dir
* phase insert <after> <description> Insert decimal phase after existing
* phase remove <phase> [--force] Remove phase, renumber all subsequent
* phase complete <phase> Mark phase done, update state + roadmap
@@ -62,6 +65,9 @@
* Todos:
* todo complete <filename> Move todo from pending to completed
*
* UAT Audit:
* audit-uat Scan all phases for unresolved UAT/verification items
*
* Scaffolding:
* scaffold context --phase <N> Create CONTEXT.md template
* scaffold uat --phase <N> Create UAT.md template
@@ -129,7 +135,7 @@
const fs = require('fs');
const path = require('path');
const { error } = require('./lib/core.cjs');
const { error, findProjectRoot } = require('./lib/core.cjs');
const state = require('./lib/state.cjs');
const phase = require('./lib/phase.cjs');
const roadmap = require('./lib/roadmap.cjs');
@@ -168,6 +174,13 @@ async function main() {
error(`Invalid --cwd: ${cwd}`);
}
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree
const { resolveWorktreeRoot } = require('./lib/core.cjs');
const worktreeRoot = resolveWorktreeRoot(cwd);
if (worktreeRoot !== cwd) {
cwd = worktreeRoot;
}
const rawIndex = args.indexOf('--raw');
const raw = rawIndex !== -1;
if (rawIndex !== -1) args.splice(rawIndex, 1);
@@ -178,6 +191,17 @@ async function main() {
error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init');
}
// Multi-repo guard: resolve project root for commands that read/write .planning/.
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
// filesystem traversal on every invocation.
const SKIP_ROOT_RESOLUTION = new Set([
'generate-slug', 'current-timestamp', 'verify-path-exists',
'verify-summary', 'template', 'frontmatter',
]);
if (!SKIP_ROOT_RESOLUTION.has(command)) {
cwd = findProjectRoot(cwd);
}
switch (command) {
case 'state': {
const subcommand = args[1];
@@ -255,6 +279,21 @@ async function main() {
plansIdx !== -1 ? parseInt(args[plansIdx + 1], 10) : null,
raw
);
} else if (subcommand === 'signal-waiting') {
const typeIdx = args.indexOf('--type');
const qIdx = args.indexOf('--question');
const optIdx = args.indexOf('--options');
const phaseIdx = args.indexOf('--phase');
state.cmdSignalWaiting(
cwd,
typeIdx !== -1 ? args[typeIdx + 1] : null,
qIdx !== -1 ? args[qIdx + 1] : null,
optIdx !== -1 ? args[optIdx + 1] : null,
phaseIdx !== -1 ? args[phaseIdx + 1] : null,
raw
);
} else if (subcommand === 'signal-resume') {
state.cmdSignalResume(cwd, raw);
} else {
state.cmdStateLoad(cwd, raw);
}
@@ -273,6 +312,7 @@ async function main() {
case 'commit': {
const amend = args.includes('--amend');
const noVerify = args.includes('--no-verify');
const filesIndex = args.indexOf('--files');
// Collect all positional args between command name and first flag,
// then join them — handles both quoted ("multi word msg") and
@@ -281,7 +321,15 @@ async function main() {
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
const message = messageArgs.join(' ') || undefined;
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
commands.cmdCommit(cwd, message, files, raw, amend);
commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
break;
}
case 'commit-to-subrepo': {
const message = args[1];
const filesIndex = args.indexOf('--files');
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
commands.cmdCommitToSubrepo(cwd, message, files, raw);
break;
}
@@ -457,7 +505,18 @@ async function main() {
if (subcommand === 'next-decimal') {
phase.cmdPhaseNextDecimal(cwd, args[2], raw);
} else if (subcommand === 'add') {
phase.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
const idIdx = args.indexOf('--id');
let customId = null;
const descArgs = [];
for (let i = 2; i < args.length; i++) {
if (args[i] === '--id' && i + 1 < args.length) {
customId = args[i + 1];
i++; // skip value
} else {
descArgs.push(args[i]);
}
}
phase.cmdPhaseAdd(cwd, descArgs.join(' '), raw, customId);
} else if (subcommand === 'insert') {
phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
} else if (subcommand === 'remove') {
@@ -512,6 +571,12 @@ async function main() {
break;
}
case 'audit-uat': {
const uat = require('./lib/uat.cjs');
uat.cmdAuditUat(cwd, raw);
break;
}
case 'stats': {
const subcommand = args[1] || 'json';
commands.cmdStats(cwd, subcommand, raw);
@@ -522,8 +587,10 @@ async function main() {
const subcommand = args[1];
if (subcommand === 'complete') {
commands.cmdTodoComplete(cwd, args[2], raw);
} else if (subcommand === 'match-phase') {
commands.cmdTodoMatchPhase(cwd, args[2], raw);
} else {
error('Unknown todo subcommand. Available: complete');
error('Unknown todo subcommand. Available: complete, match-phase');
}
break;
}

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
@@ -71,9 +71,9 @@ function cmdListTodos(cwd, area, raw) {
area: todoArea,
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
});
} catch {}
} catch { /* intentionally empty */ }
}
} catch {}
} catch { /* intentionally empty */ }
const result = { count, todos };
output(result, raw, count.toString());
@@ -98,7 +98,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
}
function cmdHistoryDigest(cwd, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
// Collect all phase directories: archived + current
@@ -120,7 +120,7 @@ function cmdHistoryDigest(cwd, raw) {
for (const dir of currentDirs) {
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
}
} catch {}
} catch { /* intentionally empty */ }
}
if (allPhaseDirs.length === 0) {
@@ -214,7 +214,7 @@ function cmdResolveModel(cwd, agentType, raw) {
output(result, raw, model);
}
function cmdCommit(cwd, message, files, raw, amend) {
function cmdCommit(cwd, message, files, raw, amend, noVerify) {
if (!message && !amend) {
error('commit message required');
}
@@ -238,11 +238,18 @@ function cmdCommit(cwd, message, files, raw, amend) {
// Stage files
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
for (const file of filesToStage) {
execGit(cwd, ['add', file]);
const fullPath = path.join(cwd, file);
if (!fs.existsSync(fullPath)) {
// File was deleted/moved — stage the deletion
execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]);
} else {
execGit(cwd, ['add', file]);
}
}
// Commit
// Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
if (noVerify) commitArgs.push('--no-verify');
const commitResult = execGit(cwd, commitArgs);
if (commitResult.exitCode !== 0) {
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
@@ -262,6 +269,74 @@ function cmdCommit(cwd, message, files, raw, amend) {
output(result, raw, hash || 'committed');
}
function cmdCommitToSubrepo(cwd, message, files, raw) {
if (!message) {
error('commit message required');
}
const config = loadConfig(cwd);
const subRepos = config.sub_repos;
if (!subRepos || subRepos.length === 0) {
error('no sub_repos configured in .planning/config.json');
}
if (!files || files.length === 0) {
error('--files required for commit-to-subrepo');
}
// Group files by sub-repo prefix
const grouped = {};
const unmatched = [];
for (const file of files) {
const match = subRepos.find(repo => file.startsWith(repo + '/'));
if (match) {
if (!grouped[match]) grouped[match] = [];
grouped[match].push(file);
} else {
unmatched.push(file);
}
}
if (unmatched.length > 0) {
process.stderr.write(`Warning: ${unmatched.length} file(s) did not match any sub-repo prefix: ${unmatched.join(', ')}\n`);
}
const repos = {};
for (const [repo, repoFiles] of Object.entries(grouped)) {
const repoCwd = path.join(cwd, repo);
// Stage files (strip sub-repo prefix for paths relative to that repo)
for (const file of repoFiles) {
const relativePath = file.slice(repo.length + 1);
execGit(repoCwd, ['add', relativePath]);
}
// Commit
const commitResult = execGit(repoCwd, ['commit', '-m', message]);
if (commitResult.exitCode !== 0) {
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
continue;
}
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr };
continue;
}
// Get hash
const hashResult = execGit(repoCwd, ['rev-parse', '--short', 'HEAD']);
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
repos[repo] = { committed: true, hash, files: repoFiles };
}
const result = {
committed: Object.values(repos).some(r => r.committed),
repos,
unmatched: unmatched.length > 0 ? unmatched : undefined,
};
output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' '));
}
function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
if (!summaryPath) {
error('summary-path required for summary-extract');
@@ -295,7 +370,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
// Build full result
const fullResult = {
path: summaryPath,
one_liner: fm['one-liner'] || null,
one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null,
key_files: fm['key-files'] || [],
tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
patterns: fm['patterns-established'] || [],
@@ -381,8 +456,8 @@ async function cmdWebsearch(query, options, raw) {
}
function cmdProgressRender(cwd, format, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const phasesDir = planningPaths(cwd).phases;
const roadmapPath = planningPaths(cwd).roadmap;
const milestone = getMilestoneInfo(cwd);
const phases = [];
@@ -412,7 +487,7 @@ function cmdProgressRender(cwd, format, raw) {
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
}
} catch {}
} catch { /* intentionally empty */ }
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
@@ -448,6 +523,130 @@ function cmdProgressRender(cwd, format, raw) {
}
}
/**
* Match pending todos against a phase's goal/name/requirements.
* Returns todos with relevance scores based on keyword, area, and file overlap.
* Used by discuss-phase to surface relevant todos before scope-setting.
*/
function cmdTodoMatchPhase(cwd, phase, raw) {
if (!phase) { error('phase required for todo match-phase'); }
const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
const todos = [];
// Load pending todos
try {
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
for (const file of files) {
try {
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
const titleMatch = content.match(/^title:\s*(.+)$/m);
const areaMatch = content.match(/^area:\s*(.+)$/m);
const filesMatch = content.match(/^files:\s*(.+)$/m);
const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
todos.push({
file,
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
area: areaMatch ? areaMatch[1].trim() : 'general',
files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
body: body.slice(0, 200), // first 200 chars for context
});
} catch {}
}
} catch {}
if (todos.length === 0) {
output({ phase, matches: [], todo_count: 0 }, raw);
return;
}
// Load phase goal/name from ROADMAP
const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : '';
const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : '';
const phaseSection = phaseInfo ? (phaseInfo.section || '') : '';
// Build keyword set from phase name + goal + section text
const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']);
const phaseKeywords = new Set(
phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
.map(w => w.replace(/[^a-z0-9]/g, ''))
.filter(w => w.length > 2 && !stopWords.has(w))
);
// Find phase directory to get expected file paths
const phaseInfoDisk = findPhaseInternal(cwd, phase);
const phasePlans = [];
if (phaseInfoDisk && phaseInfoDisk.found) {
try {
const phaseDir = path.join(cwd, phaseInfoDisk.directory);
const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
for (const pf of planFiles) {
try {
const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
if (fmFiles) {
phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
}
} catch {}
}
} catch {}
}
// Score each todo for relevance
const matches = [];
for (const todo of todos) {
let score = 0;
const reasons = [];
// Keyword match: todo title/body terms in phase text
const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
.split(/[\s\-_/.,;:()\[\]{}|]+/)
.map(w => w.replace(/[^a-z0-9]/g, ''))
.filter(w => w.length > 2 && !stopWords.has(w));
const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
if (matchedKeywords.length > 0) {
score += Math.min(matchedKeywords.length * 0.2, 0.6);
reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
}
// Area match: todo area appears in phase text
if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
score += 0.3;
reasons.push(`area: ${todo.area}`);
}
// File match: todo files overlap with phase plan files
if (todo.files.length > 0 && phasePlans.length > 0) {
const fileOverlap = todo.files.filter(f =>
phasePlans.some(pf => pf.includes(f) || f.includes(pf))
);
if (fileOverlap.length > 0) {
score += 0.4;
reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
}
}
if (score > 0) {
matches.push({
file: todo.file,
title: todo.title,
area: todo.area,
score: Math.round(score * 100) / 100,
reasons,
});
}
}
// Sort by score descending
matches.sort((a, b) => b.score - a.score);
output({ phase, matches, todo_count: todos.length }, raw);
}
function cmdTodoComplete(cwd, filename, raw) {
if (!filename) {
error('filename required for todo complete');
@@ -512,7 +711,7 @@ function cmdScaffold(cwd, type, options, raw) {
}
const slug = generateSlugInternal(name);
const dirName = `${padded}-${slug}`;
const phasesParent = path.join(cwd, '.planning', 'phases');
const phasesParent = planningPaths(cwd).phases;
fs.mkdirSync(phasesParent, { recursive: true });
const dirPath = path.join(phasesParent, dirName);
fs.mkdirSync(dirPath, { recursive: true });
@@ -534,10 +733,10 @@ function cmdScaffold(cwd, type, options, raw) {
}
function cmdStats(cwd, format, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const statePath = path.join(cwd, '.planning', 'STATE.md');
const phasesDir = planningPaths(cwd).phases;
const roadmapPath = planningPaths(cwd).roadmap;
const reqPath = planningPaths(cwd).requirements;
const statePath = planningPaths(cwd).state;
const milestone = getMilestoneInfo(cwd);
const isDirInMilestone = getMilestonePhaseFilter(cwd);
@@ -547,7 +746,7 @@ function cmdStats(cwd, format, raw) {
let totalSummaries = 0;
try {
const roadmapContent = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const roadmapContent = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let match;
while ((match = headingPattern.exec(roadmapContent)) !== null) {
@@ -559,7 +758,7 @@ function cmdStats(cwd, format, raw) {
status: 'Not Started',
});
}
} catch {}
} catch { /* intentionally empty */ }
try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
@@ -595,7 +794,7 @@ function cmdStats(cwd, format, raw) {
status,
});
}
} catch {}
} catch { /* intentionally empty */ }
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
const completedPhases = phases.filter(p => p.status === 'Complete').length;
@@ -613,7 +812,7 @@ function cmdStats(cwd, format, raw) {
requirementsComplete = checked ? checked.length : 0;
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
}
} catch {}
} catch { /* intentionally empty */ }
// Last activity from STATE.md
let lastActivity = null;
@@ -626,7 +825,7 @@ function cmdStats(cwd, format, raw) {
|| stateContent.match(/^Last activity:\s*(.+)$/im);
if (activityMatch) lastActivity = activityMatch[1].trim();
}
} catch {}
} catch { /* intentionally empty */ }
// Git stats
let gitCommits = 0;
@@ -700,10 +899,12 @@ module.exports = {
cmdHistoryDigest,
cmdResolveModel,
cmdCommit,
cmdCommitToSubrepo,
cmdSummaryExtract,
cmdWebsearch,
cmdProgressRender,
cmdTodoComplete,
cmdTodoMatchPhase,
cmdScaffold,
cmdStats,
};

View File

@@ -17,8 +17,9 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
'workflow.text_mode',
'workflow._auto_chain_active',
'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template',
'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored',
'hooks.context_warnings',
]);
@@ -88,6 +89,7 @@ function buildNewProjectConfig(userChoices) {
branching_strategy: 'none',
phase_branch_template: 'gsd/phase-{phase}-{slug}',
milestone_branch_template: 'gsd/{milestone}-{slug}',
quick_branch_template: null,
},
workflow: {
research: true,
@@ -99,6 +101,7 @@ function buildNewProjectConfig(userChoices) {
node_repair_budget: 2,
ui_phase: true,
ui_safety_gate: true,
text_mode: false,
},
hooks: {
context_warnings: true,

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');
const { execSync, execFileSync, spawnSync } = require('child_process');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
// ─── Path helpers ────────────────────────────────────────────────────────────
@@ -14,6 +14,102 @@ function toPosixPath(p) {
return p.split(path.sep).join('/');
}
/**
* Scan immediate child directories for separate git repos.
* Returns a sorted array of directory names that have their own `.git`.
* Excludes hidden directories and node_modules.
*/
function detectSubRepos(cwd) {
const results = [];
try {
const entries = fs.readdirSync(cwd, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
const gitPath = path.join(cwd, entry.name, '.git');
try {
if (fs.existsSync(gitPath)) {
results.push(entry.name);
}
} catch {}
}
} catch {}
return results.sort();
}
/**
* Walk up from `startDir` to find the project root that owns `.planning/`.
*
* In multi-repo workspaces, Claude may open inside a sub-repo (e.g. `backend/`)
* instead of the project root. This function prevents `.planning/` from being
* created inside the sub-repo by locating the nearest ancestor that already has
* a `.planning/` directory.
*
* Detection strategy (checked in order for each ancestor):
* 1. Parent has `.planning/config.json` with `sub_repos` listing this directory
* 2. Parent has `.planning/config.json` with `multiRepo: true` (legacy format)
* 3. Parent has `.planning/` and current dir has its own `.git` (heuristic)
*
* Returns `startDir` unchanged when no ancestor `.planning/` is found (first-run
* or single-repo projects).
*/
function findProjectRoot(startDir) {
const resolved = path.resolve(startDir);
const root = path.parse(resolved).root;
const homedir = require('os').homedir();
// Check if startDir or any of its ancestors (up to but not including a
// candidate project root) contains a .git directory. This handles both
// `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside).
function isInsideGitRepo(candidateParent) {
let d = resolved;
while (d !== candidateParent && d !== root) {
if (fs.existsSync(path.join(d, '.git'))) return true;
d = path.dirname(d);
}
return false;
}
let dir = resolved;
while (dir !== root) {
const parent = path.dirname(dir);
if (parent === dir) break; // filesystem root
if (parent === homedir) break; // never go above home
const parentPlanning = path.join(parent, '.planning');
if (fs.existsSync(parentPlanning) && fs.statSync(parentPlanning).isDirectory()) {
const configPath = path.join(parentPlanning, 'config.json');
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const subRepos = config.sub_repos || config.planning?.sub_repos || [];
// Check explicit sub_repos list
if (Array.isArray(subRepos) && subRepos.length > 0) {
const relPath = path.relative(parent, resolved);
const topSegment = relPath.split(path.sep)[0];
if (subRepos.includes(topSegment)) {
return parent;
}
}
// Check legacy multiRepo flag
if (config.multiRepo === true && isInsideGitRepo(parent)) {
return parent;
}
} catch {
// config.json missing or malformed — fall back to .git heuristic
}
// Heuristic: parent has .planning/ and we're inside a git repo
if (isInsideGitRepo(parent)) {
return parent;
}
}
dir = parent;
}
return startDir;
}
// ─── Output helpers ───────────────────────────────────────────────────────────
function output(result, raw, rawValue) {
@@ -58,12 +154,18 @@ function loadConfig(cwd) {
branching_strategy: 'none',
phase_branch_template: 'gsd/phase-{phase}-{slug}',
milestone_branch_template: 'gsd/{milestone}-{slug}',
quick_branch_template: null,
research: true,
plan_checker: true,
verifier: true,
nyquist_validation: true,
parallelization: true,
brave_search: false,
text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus
sub_repos: [],
resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs
context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
};
try {
@@ -75,6 +177,39 @@ function loadConfig(cwd) {
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
delete parsed.depth;
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ }
}
// Auto-detect and sync sub_repos: scan for child directories with .git
let configDirty = false;
// Migrate legacy "multiRepo: true" boolean → sub_repos array
if (parsed.multiRepo === true && !parsed.sub_repos && !parsed.planning?.sub_repos) {
const detected = detectSubRepos(cwd);
if (detected.length > 0) {
parsed.sub_repos = detected;
if (!parsed.planning) parsed.planning = {};
parsed.planning.commit_docs = false;
delete parsed.multiRepo;
configDirty = true;
}
}
// Keep sub_repos in sync with actual filesystem
const currentSubRepos = parsed.sub_repos || parsed.planning?.sub_repos || [];
if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) {
const detected = detectSubRepos(cwd);
if (detected.length > 0) {
const sorted = [...currentSubRepos].sort();
if (JSON.stringify(sorted) !== JSON.stringify(detected)) {
parsed.sub_repos = detected;
configDirty = true;
}
}
}
// Persist sub_repos changes (migration or sync)
if (configDirty) {
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
}
@@ -100,12 +235,18 @@ function loadConfig(cwd) {
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
parallelization,
brave_search: get('brave_search') ?? defaults.brave_search,
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
context_window: get('context_window') ?? defaults.context_window,
phase_naming: get('phase_naming') ?? defaults.phase_naming,
model_overrides: parsed.model_overrides || null,
};
} catch {
@@ -121,7 +262,9 @@ function isGitIgnored(cwd, targetPath) {
// Without it, git check-ignore returns "not ignored" for tracked files even when
// .gitignore explicitly lists them — a common source of confusion when .planning/
// was committed before being added to .gitignore.
execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
// Use execFileSync (array args) to prevent shell interpretation of special characters
// in file paths — avoids command injection via crafted path names.
execFileSync('git', ['check-ignore', '-q', '--no-index', '--', targetPath], {
cwd,
stdio: 'pipe',
});
@@ -248,6 +391,105 @@ function execGit(cwd, args) {
};
}
// ─── Common path helpers ──────────────────────────────────────────────────────
/**
* Resolve the main worktree root when running inside a git worktree.
* In a linked worktree, .planning/ lives in the main worktree, not in the linked one.
* Returns the main worktree path, or cwd if not in a worktree.
*/
function resolveWorktreeRoot(cwd) {
// Check if we're in a linked worktree
const gitDir = execGit(cwd, ['rev-parse', '--git-dir']);
const commonDir = execGit(cwd, ['rev-parse', '--git-common-dir']);
if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return cwd;
// In a linked worktree, .git is a file pointing to .git/worktrees/<name>
// and git-common-dir points to the main repo's .git directory
const gitDirResolved = path.resolve(cwd, gitDir.stdout);
const commonDirResolved = path.resolve(cwd, commonDir.stdout);
if (gitDirResolved !== commonDirResolved) {
// We're in a linked worktree — resolve main worktree root
// The common dir is the main repo's .git, so its parent is the main worktree root
return path.dirname(commonDirResolved);
}
return cwd;
}
/**
* Acquire a file-based lock for .planning/ writes.
* Prevents concurrent worktrees from corrupting shared planning files.
* Lock is auto-released after the callback completes.
*/
function withPlanningLock(cwd, fn) {
const lockPath = path.join(planningDir(cwd), '.lock');
const lockTimeout = 10000; // 10 seconds
const retryDelay = 100;
const start = Date.now();
// Ensure .planning/ exists
try { fs.mkdirSync(planningDir(cwd), { recursive: true }); } catch { /* ok */ }
while (Date.now() - start < lockTimeout) {
try {
// Atomic create — fails if file exists
fs.writeFileSync(lockPath, JSON.stringify({
pid: process.pid,
cwd,
acquired: new Date().toISOString(),
}), { flag: 'wx' });
// Lock acquired — run the function
try {
return fn();
} finally {
try { fs.unlinkSync(lockPath); } catch { /* already released */ }
}
} catch (err) {
if (err.code === 'EEXIST') {
// Lock exists — check if stale (>30s old)
try {
const stat = fs.statSync(lockPath);
if (Date.now() - stat.mtimeMs > 30000) {
fs.unlinkSync(lockPath);
continue; // retry
}
} catch { continue; }
// Wait and retry
spawnSync('sleep', ['0.1'], { stdio: 'ignore' });
continue;
}
throw err;
}
}
// Timeout — force acquire (stale lock recovery)
try { fs.unlinkSync(lockPath); } catch { /* ok */ }
return fn();
}
/** Get the .planning directory path */
function planningDir(cwd) {
return path.join(cwd, '.planning');
}
/** Get common .planning file paths */
function planningPaths(cwd) {
const base = path.join(cwd, '.planning');
return {
planning: base,
state: path.join(base, 'STATE.md'),
roadmap: path.join(base, 'ROADMAP.md'),
project: path.join(base, 'PROJECT.md'),
config: path.join(base, 'config.json'),
phases: path.join(base, 'phases'),
requirements: path.join(base, 'REQUIREMENTS.md'),
};
}
// ─── Phase utilities ──────────────────────────────────────────────────────────
function escapeRegex(value) {
@@ -255,17 +497,23 @@ function escapeRegex(value) {
}
function normalizePhaseName(phase) {
const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
if (!match) return phase;
const padded = match[1].padStart(2, '0');
const letter = match[2] ? match[2].toUpperCase() : '';
const decimal = match[3] || '';
return padded + letter + decimal;
const str = String(phase);
// Standard numeric phases: 1, 01, 12A, 12.1
const match = str.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
if (match) {
const padded = match[1].padStart(2, '0');
const letter = match[2] ? match[2].toUpperCase() : '';
const decimal = match[3] || '';
return padded + letter + decimal;
}
// Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is
return str;
}
function comparePhaseNum(a, b) {
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
// If either is non-numeric (custom ID), fall back to string comparison
if (!pa || !pb) return String(a).localeCompare(String(b));
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
if (intDiff !== 0) return intDiff;
@@ -295,10 +543,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
try {
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
const match = dirs.find(d => d.startsWith(normalized));
// Match: starts with normalized (numeric) OR contains normalized as prefix segment (custom ID)
const match = dirs.find(d => {
if (d.startsWith(normalized)) return true;
// For custom IDs like PROJ-42, match case-insensitively
if (d.toUpperCase().startsWith(normalized.toUpperCase())) return true;
return false;
});
if (!match) return null;
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
// Extract phase number and name — supports both numeric (01-name) and custom (PROJ-42-name)
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|| match.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-(.+)/i)
|| [null, match, null];
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
const phaseDir = path.join(baseDir, match);
@@ -368,7 +625,7 @@ function findPhaseInternal(cwd, phase) {
return result;
}
}
} catch {}
} catch { /* intentionally empty */ }
return null;
}
@@ -403,7 +660,7 @@ function getArchivedPhaseDirs(cwd) {
});
}
}
} catch {}
} catch { /* intentionally empty */ }
return results;
}
@@ -420,6 +677,91 @@ function stripShippedMilestones(content) {
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
}
/**
* Extract the current milestone section from ROADMAP.md by positive lookup.
*
* Instead of stripping <details> blocks (negative heuristic that breaks if
* agents wrap the current milestone in <details>), this finds the section
* matching the current milestone version and returns only that content.
*
* Falls back to stripShippedMilestones() if:
* - cwd is not provided
* - STATE.md doesn't exist or has no milestone field
* - Version can't be found in ROADMAP.md
*
* @param {string} content - Full ROADMAP.md content
* @param {string} [cwd] - Working directory for reading STATE.md
* @returns {string} Content scoped to current milestone
*/
function extractCurrentMilestone(content, cwd) {
if (!cwd) return stripShippedMilestones(content);
// 1. Get current milestone version from STATE.md frontmatter
let version = null;
try {
const statePath = path.join(cwd, '.planning', 'STATE.md');
if (fs.existsSync(statePath)) {
const stateRaw = fs.readFileSync(statePath, 'utf-8');
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
if (milestoneMatch) {
version = milestoneMatch[1].trim();
}
}
} catch {}
// 2. Fallback: derive version from getMilestoneInfo pattern in ROADMAP.md itself
if (!version) {
// Check for 🚧 in-progress marker
const inProgressMatch = content.match(/🚧\s*\*\*v(\d+\.\d+)\s/);
if (inProgressMatch) {
version = 'v' + inProgressMatch[1];
}
}
if (!version) return stripShippedMilestones(content);
// 3. Find the section matching this version
// Match headings like: ## Roadmap v3.0: Name, ## v3.0 Name, etc.
const escapedVersion = escapeRegex(version);
const sectionPattern = new RegExp(
`(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`,
'mi'
);
const sectionMatch = content.match(sectionPattern);
if (!sectionMatch) return stripShippedMilestones(content);
const sectionStart = sectionMatch.index;
// Find the end: next milestone heading at same or higher level, or EOF
// Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc.
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
const restContent = content.slice(sectionStart + sectionMatch[0].length);
const nextMilestonePattern = new RegExp(
`^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`,
'mi'
);
const nextMatch = restContent.match(nextMilestonePattern);
let sectionEnd;
if (nextMatch) {
sectionEnd = sectionStart + sectionMatch[0].length + nextMatch.index;
} else {
sectionEnd = content.length;
}
// Return everything before the current milestone section (non-milestone content
// like title, overview) plus the current milestone section
const beforeMilestones = content.slice(0, sectionStart);
const currentSection = content.slice(sectionStart, sectionEnd);
// Also include any content before the first milestone heading (title, overview, etc.)
// but strip any <details> blocks in it (these are definitely shipped)
const preamble = beforeMilestones.replace(/<details>[\s\S]*?<\/details>/gi, '');
return preamble + currentSection;
}
/**
* Replace a pattern only in the current milestone section of ROADMAP.md
* (everything after the last </details> close tag). Used for write operations
@@ -444,8 +786,9 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
if (!fs.existsSync(roadmapPath)) return null;
try {
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const escapedPhase = escapeRegex(phaseNum.toString());
// Match both numeric (Phase 1:) and custom (Phase PROJ-42:) headers
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
const headerMatch = content.match(phasePattern);
if (!headerMatch) return null;
@@ -453,7 +796,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
const phaseName = headerMatch[1].trim();
const headerIndex = headerMatch.index;
const restOfContent = content.slice(headerIndex);
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+[\w]/i);
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
const section = content.slice(headerIndex, sectionEnd).trim();
@@ -472,6 +815,19 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
}
}
// ─── Model alias resolution ───────────────────────────────────────────────────
/**
* Map short model aliases to full model IDs.
* Updated each release to match current model versions.
* Users can override with model_overrides in config.json for custom/latest models.
*/
const MODEL_ALIAS_MAP = {
'opus': 'claude-opus-4-0',
'sonnet': 'claude-sonnet-4-5',
'haiku': 'claude-haiku-3-5',
};
function resolveModelInternal(cwd, agentType) {
const config = loadConfig(cwd);
@@ -486,7 +842,32 @@ function resolveModelInternal(cwd, agentType) {
const agentModels = MODEL_PROFILES[agentType];
if (!agentModels) return 'sonnet';
if (profile === 'inherit') return 'inherit';
return agentModels[profile] || agentModels['balanced'] || 'sonnet';
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
// If resolve_model_ids is true, map alias to full model ID
// This prevents 404s when the Task tool passes aliases directly to the API
if (config.resolve_model_ids) {
return MODEL_ALIAS_MAP[alias] || alias;
}
return alias;
}
// ─── Summary body helpers ─────────────────────────────────────────────────
/**
* Extract a one-liner from the summary body when it's not in frontmatter.
* The summary template defines one-liner as a bold markdown line after the heading:
* # Phase X: Name Summary
* **[substantive one-liner text]**
*/
function extractOneLinerFromBody(content) {
if (!content) return null;
// Strip frontmatter first
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
// Find the first **...** line after a # heading
const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m);
return match ? match[1].trim() : null;
}
// ─── Misc utilities ───────────────────────────────────────────────────────────
@@ -512,7 +893,8 @@ function getMilestoneInfo(cwd) {
// First: check for list-format roadmaps using 🚧 (in-progress) marker
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
// e.g. "- 🚧 **v1.2.1 Tech Debt** — Phases 1-8 (in progress)"
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
if (inProgressMatch) {
return {
version: 'v' + inProgressMatch[1],
@@ -523,15 +905,16 @@ function getMilestoneInfo(cwd) {
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
const cleaned = stripShippedMilestones(roadmap);
// Extract version and name from the same ## heading for consistency
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
// Supports 2+ segment versions: v1.2, v1.2.1, v2.0.1, etc.
const headingMatch = cleaned.match(/## .*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/);
if (headingMatch) {
return {
version: 'v' + headingMatch[1],
name: headingMatch[2].trim(),
};
}
// Fallback: try bare version match
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
// Fallback: try bare version match (greedy — capture longest version string)
const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
return {
version: versionMatch ? versionMatch[0] : 'v1.0',
name: 'milestone',
@@ -549,13 +932,14 @@ function getMilestoneInfo(cwd) {
function getMilestonePhaseFilter(cwd) {
const milestonePhaseNums = new Set();
try {
const roadmap = stripShippedMilestones(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'));
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd);
// Match both numeric phases (Phase 1:) and custom IDs (Phase PROJ-42:)
const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
let m;
while ((m = phasePattern.exec(roadmap)) !== null) {
milestonePhaseNums.add(m[1]);
}
} catch {}
} catch { /* intentionally empty */ }
if (milestonePhaseNums.size === 0) {
const passAll = () => true;
@@ -568,9 +952,13 @@ function getMilestonePhaseFilter(cwd) {
);
function isDirInMilestone(dirName) {
// Try numeric match first
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
if (!m) return false;
return normalized.has(m[1].toLowerCase());
if (m && normalized.has(m[1].toLowerCase())) return true;
// Try custom ID match (e.g. PROJ-42-description → PROJ-42)
const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
if (customMatch && normalized.has(customMatch[1].toLowerCase())) return true;
return false;
}
isDirInMilestone.phaseCount = milestonePhaseNums.size;
return isDirInMilestone;
@@ -597,6 +985,15 @@ module.exports = {
getMilestoneInfo,
getMilestonePhaseFilter,
stripShippedMilestones,
extractCurrentMilestone,
replaceInCurrentMilestone,
toPosixPath,
extractOneLinerFromBody,
resolveWorktreeRoot,
withPlanningLock,
findProjectRoot,
detectSubRepos,
MODEL_ALIAS_MAP,
planningDir,
planningPaths,
};

View File

@@ -10,7 +10,11 @@ const { safeReadFile, normalizeMd, output, error } = require('./core.cjs');
function extractFrontmatter(content) {
const frontmatter = {};
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
// Find ALL frontmatter blocks at the start of the file.
// If multiple blocks exist (corruption from CRLF mismatch), use the LAST one
// since it represents the most recent state sync.
const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)];
const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
if (!match) return frontmatter;
const yaml = match[1];

View File

@@ -5,7 +5,34 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
function getLatestCompletedMilestone(cwd) {
const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
if (!fs.existsSync(milestonesPath)) return null;
try {
const content = fs.readFileSync(milestonesPath, 'utf-8');
const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m);
if (!match) return null;
return {
version: match[1],
name: match[2].trim(),
};
} catch {
return null;
}
}
/**
* Inject `project_root` into an init result object.
* Workflows use this to prefix `.planning/` paths correctly when Claude's CWD
* differs from the project root (e.g., inside a sub-repo).
*/
function withProjectRoot(cwd, result) {
result.project_root = cwd;
return result;
}
function cmdInitExecutePhase(cwd, phase, raw) {
if (!phase) {
@@ -30,7 +57,9 @@ function cmdInitExecutePhase(cwd, phase, raw) {
// Config flags
commit_docs: config.commit_docs,
sub_repos: config.sub_repos,
parallelization: config.parallelization,
context_window: config.context_window,
branching_strategy: config.branching_strategy,
phase_branch_template: config.phase_branch_template,
milestone_branch_template: config.milestone_branch_template,
@@ -77,7 +106,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
config_path: '.planning/config.json',
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitPlanPhase(cwd, phase, raw) {
@@ -153,10 +182,10 @@ function cmdInitPlanPhase(cwd, phase, raw) {
if (uatFile) {
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
}
} catch {}
} catch { /* intentionally empty */ }
}
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitNewProject(cwd, raw) {
@@ -167,17 +196,26 @@ function cmdInitNewProject(cwd, raw) {
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
// Detect existing code
// Detect existing code (cross-platform — no Unix `find` dependency)
let hasCode = false;
let hasPackageFile = false;
try {
const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
hasCode = files.trim().length > 0;
} catch {}
const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']);
function findCodeFiles(dir, depth) {
if (depth > 3) return false;
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
for (const entry of entries) {
if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
if (entry.isDirectory() && !skipDirs.has(entry.name)) {
if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
}
}
return false;
}
hasCode = findCodeFiles(cwd, 0);
} catch { /* intentionally empty — best-effort detection */ }
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
pathExistsInternal(cwd, 'requirements.txt') ||
@@ -215,12 +253,23 @@ function cmdInitNewProject(cwd, raw) {
project_path: '.planning/PROJECT.md',
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitNewMilestone(cwd, raw) {
const config = loadConfig(cwd);
const milestone = getMilestoneInfo(cwd);
const latestCompleted = getLatestCompletedMilestone(cwd);
const phasesDir = path.join(cwd, '.planning', 'phases');
let phaseDirCount = 0;
try {
if (fs.existsSync(phasesDir)) {
phaseDirCount = fs.readdirSync(phasesDir, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.length;
}
} catch {}
const result = {
// Models
@@ -235,6 +284,10 @@ function cmdInitNewMilestone(cwd, raw) {
// Current milestone
current_milestone: milestone.version,
current_milestone_name: milestone.name,
latest_completed_milestone: latestCompleted?.version || null,
latest_completed_milestone_name: latestCompleted?.name || null,
phase_dir_count: phaseDirCount,
phase_archive_path: latestCompleted ? `.planning/milestones/${latestCompleted.version}-phases` : null,
// File existence
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
@@ -247,7 +300,7 @@ function cmdInitNewMilestone(cwd, raw) {
state_path: '.planning/STATE.md',
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitQuick(cwd, description, raw) {
@@ -267,6 +320,13 @@ function cmdInitQuick(cwd, description, raw) {
const timeBlocks = Math.floor(secondsSinceMidnight / 2);
const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
const quickId = dateStr + '-' + timeEncoded;
const branchSlug = slug || 'quick';
const quickBranchName = config.quick_branch_template
? config.quick_branch_template
.replace('{num}', quickId)
.replace('{quick}', quickId)
.replace('{slug}', branchSlug)
: null;
const result = {
// Models
@@ -277,6 +337,7 @@ function cmdInitQuick(cwd, description, raw) {
// Config
commit_docs: config.commit_docs,
branch_name: quickBranchName,
// Quick task info
quick_id: quickId,
@@ -297,7 +358,7 @@ function cmdInitQuick(cwd, description, raw) {
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitResume(cwd, raw) {
@@ -307,7 +368,7 @@ function cmdInitResume(cwd, raw) {
let interruptedAgentId = null;
try {
interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
} catch {}
} catch { /* intentionally empty */ }
const result = {
// File existence
@@ -329,7 +390,7 @@ function cmdInitResume(cwd, raw) {
commit_docs: config.commit_docs,
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitVerifyWork(cwd, phase, raw) {
@@ -358,7 +419,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
has_verification: phaseInfo?.has_verification || false,
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitPhaseOp(cwd, phase, raw) {
@@ -459,10 +520,10 @@ function cmdInitPhaseOp(cwd, phase, raw) {
if (uatFile) {
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
}
} catch {}
} catch { /* intentionally empty */ }
}
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitTodos(cwd, area, raw) {
@@ -494,9 +555,9 @@ function cmdInitTodos(cwd, area, raw) {
area: todoArea,
path: '.planning/todos/pending/' + file,
});
} catch {}
} catch { /* intentionally empty */ }
}
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Config
@@ -521,7 +582,7 @@ function cmdInitTodos(cwd, area, raw) {
pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitMilestoneOp(cwd, raw) {
@@ -543,9 +604,9 @@ function cmdInitMilestoneOp(cwd, raw) {
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
if (hasSummary) completedPhases++;
} catch {}
} catch { /* intentionally empty */ }
}
} catch {}
} catch { /* intentionally empty */ }
// Check archive
const archiveDir = path.join(cwd, '.planning', 'archive');
@@ -554,7 +615,7 @@ function cmdInitMilestoneOp(cwd, raw) {
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name);
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Config
@@ -582,7 +643,7 @@ function cmdInitMilestoneOp(cwd, raw) {
phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitMapCodebase(cwd, raw) {
@@ -593,7 +654,7 @@ function cmdInitMapCodebase(cwd, raw) {
let existingMaps = [];
try {
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Models
@@ -616,7 +677,7 @@ function cmdInitMapCodebase(cwd, raw) {
codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
function cmdInitProgress(cwd, raw) {
@@ -633,8 +694,8 @@ function cmdInitProgress(cwd, raw) {
const roadmapPhaseNums = new Set();
const roadmapPhaseNames = new Map();
try {
const roadmapContent = stripShippedMilestones(
fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8')
const roadmapContent = extractCurrentMilestone(
fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd
);
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let hm;
@@ -642,7 +703,7 @@ function cmdInitProgress(cwd, raw) {
roadmapPhaseNums.add(hm[1]);
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
}
} catch {}
} catch { /* intentionally empty */ }
const isDirInMilestone = getMilestonePhaseFilter(cwd);
const seenPhaseNums = new Set();
@@ -695,7 +756,7 @@ function cmdInitProgress(cwd, raw) {
nextPhase = phaseInfo;
}
}
} catch {}
} catch { /* intentionally empty */ }
// Add phases defined in ROADMAP but not yet scaffolded to disk
for (const [num, name] of roadmapPhaseNames) {
@@ -726,7 +787,7 @@ function cmdInitProgress(cwd, raw) {
const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
if (pauseMatch) pausedAt = pauseMatch[1].trim();
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Models
@@ -763,7 +824,7 @@ function cmdInitProgress(cwd, raw) {
config_path: '.planning/config.json',
};
output(result, raw);
output(withProjectRoot(cwd, result), raw);
}
module.exports = {

View File

@@ -4,9 +4,9 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
const { writeStateMd, stateReplaceFieldWithFallback } = require('./state.cjs');
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
if (!reqIdsRaw || reqIdsRaw.length === 0) {
@@ -25,7 +25,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
error('no valid requirement IDs found');
}
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const reqPath = planningPaths(cwd).requirements;
if (!fs.existsSync(reqPath)) {
output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
return;
@@ -90,12 +90,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
error('version required for milestone complete (e.g., v1.0)');
}
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const statePath = path.join(cwd, '.planning', 'STATE.md');
const roadmapPath = planningPaths(cwd).roadmap;
const reqPath = planningPaths(cwd).requirements;
const statePath = planningPaths(cwd).state;
const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
const archiveDir = path.join(cwd, '.planning', 'milestones');
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
const today = new Date().toISOString().split('T')[0];
const milestoneName = options.name || version;
@@ -131,16 +131,24 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
try {
const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
const fm = extractFrontmatter(content);
if (fm['one-liner']) {
accomplishments.push(fm['one-liner']);
const oneLiner = fm['one-liner'] || extractOneLinerFromBody(content);
if (oneLiner) {
accomplishments.push(oneLiner);
}
// Count tasks
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
totalTasks += taskMatches.length;
} catch {}
// Count tasks: prefer **Tasks:** N from Performance section,
// then <task XML tags, then ## Task N markdown headers
const tasksFieldMatch = content.match(/\*\*Tasks:\*\*\s*(\d+)/);
if (tasksFieldMatch) {
totalTasks += parseInt(tasksFieldMatch[1], 10);
} else {
const xmlTaskMatches = content.match(/<task[\s>]/gi) || [];
const mdTaskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
totalTasks += xmlTaskMatches.length || mdTaskMatches.length;
}
} catch { /* intentionally empty */ }
}
}
} catch {}
} catch { /* intentionally empty */ }
// Archive ROADMAP.md
if (fs.existsSync(roadmapPath)) {
@@ -186,21 +194,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
}
// Update STATE.md
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');
stateContent = stateContent.replace(
/(\*\*Status:\*\*\s*).*/,
`$1${version} milestone complete`
);
stateContent = stateContent.replace(
/(\*\*Last Activity:\*\*\s*).*/,
`$1${today}`
);
stateContent = stateContent.replace(
/(\*\*Last Activity Description:\*\*\s*).*/,
`$1${version} milestone completed and archived`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
`${version} milestone completed and archived`);
writeStateMd(statePath, stateContent, cwd);
}
@@ -220,7 +222,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
archivedCount++;
}
phasesArchived = archivedCount > 0;
} catch {}
} catch { /* intentionally empty */ }
}
const result = {

View File

@@ -4,9 +4,9 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs');
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs');
function cmdPhasesList(cwd, options, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
@@ -308,32 +308,44 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
output(result, raw);
}
function cmdPhaseAdd(cwd, description, raw) {
function cmdPhaseAdd(cwd, description, raw, customId) {
if (!description) {
error('description required for phase add');
}
const config = loadConfig(cwd);
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
if (!fs.existsSync(roadmapPath)) {
error('ROADMAP.md not found');
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const slug = generateSlugInternal(description);
// Find highest integer phase number (in current milestone only)
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
let maxPhase = 0;
let m;
while ((m = phasePattern.exec(content)) !== null) {
const num = parseInt(m[1], 10);
if (num > maxPhase) maxPhase = num;
let newPhaseId;
let dirName;
if (customId || config.phase_naming === 'custom') {
// Custom phase naming: use provided ID or generate from description
newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
if (!newPhaseId) error('--id required when phase_naming is "custom"');
dirName = `${newPhaseId}-${slug}`;
} else {
// Sequential mode: find highest integer phase number (in current milestone only)
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
let maxPhase = 0;
let m;
while ((m = phasePattern.exec(content)) !== null) {
const num = parseInt(m[1], 10);
if (num > maxPhase) maxPhase = num;
}
newPhaseId = maxPhase + 1;
const paddedNum = String(newPhaseId).padStart(2, '0');
dirName = `${paddedNum}-${slug}`;
}
const newPhaseNum = maxPhase + 1;
const paddedNum = String(newPhaseNum).padStart(2, '0');
const dirName = `${paddedNum}-${slug}`;
const dirPath = path.join(cwd, '.planning', 'phases', dirName);
// Create directory with .gitkeep so git tracks empty folders
@@ -341,7 +353,8 @@ function cmdPhaseAdd(cwd, description, raw) {
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
// Build phase entry
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseId} to break down)\n`;
// Find insertion point: before last "---" or at end
let updatedContent;
@@ -355,14 +368,15 @@ function cmdPhaseAdd(cwd, description, raw) {
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
const result = {
phase_number: newPhaseNum,
padded: paddedNum,
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
name: description,
slug,
directory: `.planning/phases/${dirName}`,
naming_mode: config.phase_naming,
};
output(result, raw, paddedNum);
output(result, raw, result.padded);
}
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
@@ -376,7 +390,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const slug = generateSlugInternal(description);
// Normalize input then strip leading zeros for flexible matching
@@ -401,7 +415,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
const dm = dir.match(decimalPattern);
if (dm) existingDecimals.push(parseInt(dm[1], 10));
}
} catch {}
} catch { /* intentionally empty */ }
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
@@ -470,7 +484,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
} catch {}
} catch { /* intentionally empty */ }
// Check for executed work (SUMMARY.md files)
if (targetDir && !force) {
@@ -538,7 +552,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
} else {
// Integer removal: renumber all subsequent integer phases
@@ -598,7 +612,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
}
// Update ROADMAP.md
@@ -671,12 +685,11 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');
// Update "Total Phases" field
const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
const totalMatch = stateContent.match(totalPattern);
if (totalMatch) {
const oldTotal = parseInt(totalMatch[2], 10);
stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`);
// Update "Total Phases" field — supports both bold and plain formats
const totalRaw = stateExtractField(stateContent, 'Total Phases');
if (totalRaw) {
const oldTotal = parseInt(totalRaw, 10);
stateContent = stateReplaceField(stateContent, 'Total Phases', String(oldTotal - 1)) || stateContent;
}
// Update "Phase: X of Y" pattern
const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
@@ -721,6 +734,27 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
const summaryCount = phaseInfo.summaries.length;
let requirementsUpdated = false;
// Check for unresolved verification debt (non-blocking warnings)
const warnings = [];
try {
const phaseFullDir = path.join(cwd, phaseInfo.directory);
const phaseFiles = fs.readdirSync(phaseFullDir);
for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
}
for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
}
} catch {}
// Update ROADMAP.md: mark phase complete
if (fs.existsSync(roadmapPath)) {
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
@@ -732,16 +766,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
);
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
// Progress table: update Status to Complete, add date
// Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
const phaseEscaped = escapeRegex(phaseNum);
const tablePattern = new RegExp(
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
'i'
);
roadmapContent = replaceInCurrentMilestone(
roadmapContent, tablePattern,
`$1 Complete $2 ${today} $3`
const tableRowPattern = new RegExp(
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
'im'
);
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
const cells = fullRow.split('|').slice(1, -1);
if (cells.length === 5) {
// 5-col: Phase | Milestone | Plans | Status | Completed
cells[3] = ' Complete ';
cells[4] = ` ${today} `;
} else if (cells.length === 4) {
// 4-col: Phase | Plans | Status | Completed
cells[2] = ' Complete ';
cells[3] = ` ${today} `;
}
return '|' + cells.join('|') + '|';
});
// Update plan count in phase section
const planCountPattern = new RegExp(
@@ -760,7 +803,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
if (fs.existsSync(reqPath)) {
// Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
const phaseEsc = escapeRegex(phaseNum);
const currentMilestoneRoadmap = stripShippedMilestones(roadmapContent);
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
const phaseSectionMatch = currentMilestoneRoadmap.match(
new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
);
@@ -818,13 +861,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// Fallback: if filesystem found no next phase, check ROADMAP.md
// for phases that are defined but not yet planned (no directory on disk)
if (isLastPhase && fs.existsSync(roadmapPath)) {
try {
const roadmapForPhases = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let pm;
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
@@ -835,50 +878,69 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
break;
}
}
} catch {}
} catch { /* intentionally empty */ }
}
// Update STATE.md
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');
// Update Current Phase
stateContent = stateContent.replace(
/(\*\*Current Phase:\*\*\s*).*/,
`$1${nextPhaseNum || phaseNum}`
);
// Update Current Phase — preserve "X of Y (Name)" compound format
const phaseValue = nextPhaseNum || phaseNum;
const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
|| stateExtractField(stateContent, 'Phase');
let newPhaseValue = String(phaseValue);
if (existingPhaseField) {
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
if (totalMatch) {
const total = totalMatch[1];
const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
}
}
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
// Update Current Phase Name
if (nextPhaseName) {
stateContent = stateContent.replace(
/(\*\*Current Phase Name:\*\*\s*).*/,
`$1${nextPhaseName.replace(/-/g, ' ')}`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
}
// Update Status
stateContent = stateContent.replace(
/(\*\*Status:\*\*\s*).*/,
`$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
isLastPhase ? 'Milestone complete' : 'Ready to plan');
// Update Current Plan
stateContent = stateContent.replace(
/(\*\*Current Plan:\*\*\s*).*/,
`$1Not started`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
// Update Last Activity
stateContent = stateContent.replace(
/(\*\*Last Activity:\*\*\s*).*/,
`$1${today}`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
// Update Last Activity Description
stateContent = stateContent.replace(
/(\*\*Last Activity Description:\*\*\s*).*/,
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
// Increment Completed Phases counter (#956)
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
if (completedRaw) {
const newCompleted = parseInt(completedRaw, 10) + 1;
stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
// Recalculate percent based on completed / total (#956)
const totalRaw = stateExtractField(stateContent, 'Total Phases');
if (totalRaw) {
const totalPhases = parseInt(totalRaw, 10);
if (totalPhases > 0) {
const newPercent = Math.round((newCompleted / totalPhases) * 100);
stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
// Also update percent field if it exists separately
stateContent = stateContent.replace(
/(percent:\s*)\d+/,
`$1${newPercent}`
);
}
}
}
writeStateMd(statePath, stateContent, cwd);
}
@@ -894,6 +956,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
roadmap_updated: fs.existsSync(roadmapPath),
state_updated: fs.existsSync(statePath),
requirements_updated: requirementsUpdated,
warnings,
has_warnings: warnings.length > 0,
};
output(result, raw);

View File

@@ -179,6 +179,17 @@ const CLAUDE_MD_FALLBACKS = {
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
};
const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
'',
'Use these entry points:',
'- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks',
'- `/gsd:debug` for investigation and bug fixing',
'- `/gsd:execute-phase` for planned phase work',
'',
'Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.',
].join('\n');
const CLAUDE_MD_PROFILE_PLACEHOLDER = [
'<!-- GSD:profile-start -->',
'## Developer Profile',
@@ -356,6 +367,14 @@ function generateArchitectureSection(cwd) {
return { content: summary, source: 'ARCHITECTURE.md', hasFallback: false };
}
function generateWorkflowSection() {
return {
content: CLAUDE_MD_WORKFLOW_ENFORCEMENT,
source: 'GSD defaults',
hasFallback: false,
};
}
// ─── Commands ─────────────────────────────────────────────────────────────────
function cmdWriteProfile(cwd, options, raw) {
@@ -796,18 +815,20 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
}
function cmdGenerateClaudeMd(cwd, options, raw) {
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture'];
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'workflow'];
const generators = {
project: generateProjectSection,
stack: generateStackSection,
conventions: generateConventionsSection,
architecture: generateArchitectureSection,
workflow: generateWorkflowSection,
};
const sectionHeadings = {
project: '## Project',
stack: '## Technology Stack',
conventions: '## Conventions',
architecture: '## Architecture',
workflow: '## GSD Workflow Enforcement',
};
const generated = {};

View File

@@ -4,10 +4,10 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, replaceInCurrentMilestone } = require('./core.cjs');
const { escapeRegex, normalizePhaseName, planningPaths, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {
output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
@@ -15,7 +15,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
}
try {
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
// Escape special regex chars in phase number, handle decimal
const escapedPhase = escapeRegex(phaseNum);
@@ -91,7 +91,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
}
function cmdRoadmapAnalyze(cwd, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {
output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
@@ -99,8 +99,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const phasesDir = path.join(cwd, '.planning', 'phases');
const content = extractCurrentMilestone(rawContent, cwd);
const phasesDir = planningPaths(cwd).phases;
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
@@ -151,7 +151,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
else if (hasContext) diskStatus = 'discussed';
else diskStatus = 'empty';
}
} catch {}
} catch { /* intentionally empty */ }
// Check ROADMAP checkbox status
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
@@ -181,7 +181,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
// Extract milestone info
const milestones = [];
const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi;
let mMatch;
while ((mMatch = milestonePattern.exec(content)) !== null) {
milestones.push({
@@ -230,7 +230,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
error('phase number required for roadmap update-plan-progress');
}
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
const phaseInfo = findPhaseInternal(cwd, phaseNum);
if (!phaseInfo) {
@@ -257,16 +257,27 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
const phaseEscaped = escapeRegex(phaseNum);
// Progress table row: update Plans column (summaries/plans) and Status column
const tablePattern = new RegExp(
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
'i'
// Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
const tableRowPattern = new RegExp(
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
'im'
);
const dateField = isComplete ? ` ${today} ` : ' ';
roadmapContent = replaceInCurrentMilestone(
roadmapContent, tablePattern,
`$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
);
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
if (cells.length === 5) {
// 5-col: Phase | Milestone | Plans | Status | Completed
cells[2] = ` ${summaryCount}/${planCount} `;
cells[3] = ` ${status.padEnd(11)}`;
cells[4] = dateField;
} else if (cells.length === 4) {
// 4-col: Phase | Plans | Status | Completed
cells[1] = ` ${summaryCount}/${planCount} `;
cells[2] = ` ${status.padEnd(11)}`;
cells[3] = dateField;
}
return '|' + cells.join('|') + '|';
});
// Update plan count in phase detail section
const planCountPattern = new RegExp(
@@ -287,6 +298,18 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
}
// Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md" or "- [ ] 50-01:")
for (const summaryFile of phaseInfo.summaries) {
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
if (!planId) continue;
const planEscaped = escapeRegex(planId);
const planCheckboxPattern = new RegExp(
`(-\\s*\\[) (\\]\\s*${planEscaped})`,
'i'
);
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
}
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
output({

View File

@@ -4,9 +4,14 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs');
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
/** Shorthand — every state command needs this path */
function getStatePath(cwd) {
return planningPaths(cwd).state;
}
// Shared helper: extract a field value from STATE.md content.
// Supports both **Field:** bold and plain Field: format.
function stateExtractField(content, fieldName) {
@@ -21,15 +26,15 @@ function stateExtractField(content, fieldName) {
function cmdStateLoad(cwd, raw) {
const config = loadConfig(cwd);
const planningDir = path.join(cwd, '.planning');
const planDir = planningPaths(cwd).planning;
let stateRaw = '';
try {
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
} catch {}
stateRaw = fs.readFileSync(path.join(planDir, 'STATE.md'), 'utf-8');
} catch { /* intentionally empty */ }
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
const configExists = fs.existsSync(path.join(planDir, 'config.json'));
const roadmapExists = fs.existsSync(path.join(planDir, 'ROADMAP.md'));
const stateExists = stateRaw.length > 0;
const result = {
@@ -65,7 +70,7 @@ function cmdStateLoad(cwd, raw) {
}
function cmdStateGet(cwd, section, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
const content = fs.readFileSync(statePath, 'utf-8');
@@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) {
}
// Try to find markdown section or field
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fieldEscaped = escapeRegex(section);
// Check for **field:** value (bold format)
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
@@ -119,13 +124,13 @@ function readTextArgOrFile(cwd, value, filePath, label) {
}
function cmdStatePatch(cwd, patches, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
let content = fs.readFileSync(statePath, 'utf-8');
const results = { updated: [], failed: [] };
for (const [field, value] of Object.entries(patches)) {
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fieldEscaped = escapeRegex(field);
// Try **Field:** bold format first, then plain Field: format
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
@@ -156,10 +161,10 @@ function cmdStateUpdate(cwd, field, value) {
error('field and value required for state update');
}
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
let content = fs.readFileSync(statePath, 'utf-8');
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fieldEscaped = escapeRegex(field);
// Try **Field:** bold format first, then plain Field: format
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
@@ -180,21 +185,10 @@ function cmdStateUpdate(cwd, field, value) {
}
// ─── State Progression Engine ────────────────────────────────────────────────
function stateExtractField(content, fieldName) {
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Try **Field:** bold format first
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
const boldMatch = content.match(boldPattern);
if (boldMatch) return boldMatch[1].trim();
// Fall back to plain Field: format
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
const plainMatch = content.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
}
// stateExtractField is defined above (shared helper) — do not duplicate.
function stateReplaceField(content, fieldName, newValue) {
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegex(fieldName);
// Try **Field:** bold format first, then plain Field: format
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
if (boldPattern.test(content)) {
@@ -207,37 +201,76 @@ function stateReplaceField(content, fieldName, newValue) {
return null;
}
/**
* Replace a STATE.md field with fallback field name support.
* Tries `primary` first, then `fallback` (if provided), returns content unchanged
* if neither matches. This consolidates the replaceWithFallback pattern that was
* previously duplicated inline across phase.cjs, milestone.cjs, and state.cjs.
*/
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
let result = stateReplaceField(content, primary, value);
if (result) return result;
if (fallback) {
result = stateReplaceField(content, fallback, value);
if (result) return result;
}
return content;
}
function cmdStateAdvancePlan(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10);
const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10);
const today = new Date().toISOString().split('T')[0];
// Try legacy separate fields first, then compound "Plan: X of Y" format
const legacyPlan = stateExtractField(content, 'Current Plan');
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
const planField = stateExtractField(content, 'Plan');
let currentPlan, totalPlans;
let useCompoundFormat = false;
if (legacyPlan && legacyTotal) {
currentPlan = parseInt(legacyPlan, 10);
totalPlans = parseInt(legacyTotal, 10);
} else if (planField) {
// Compound format: "2 of 6 in current phase" or "2 of 6"
currentPlan = parseInt(planField, 10);
const ofMatch = planField.match(/of\s+(\d+)/);
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
useCompoundFormat = true;
}
if (isNaN(currentPlan) || isNaN(totalPlans)) {
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
return;
}
if (currentPlan >= totalPlans) {
content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
content = stateReplaceField(content, 'Last Activity', today) || content;
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
writeStateMd(statePath, content, cwd);
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
} else {
const newPlan = currentPlan + 1;
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
content = stateReplaceField(content, 'Status', 'Ready to execute') || content;
content = stateReplaceField(content, 'Last Activity', today) || content;
if (useCompoundFormat) {
// Preserve compound format: "X of Y in current phase" → replace X only
const newPlanValue = planField.replace(/^\d+/, String(newPlan));
content = stateReplaceField(content, 'Plan', newPlanValue) || content;
} else {
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
}
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
writeStateMd(statePath, content, cwd);
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
}
}
function cmdStateRecordMetric(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
@@ -271,13 +304,13 @@ function cmdStateRecordMetric(cwd, options, raw) {
}
function cmdStateUpdateProgress(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
// Count summaries across current milestone phases only
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
let totalPlans = 0;
let totalSummaries = 0;
@@ -316,7 +349,7 @@ function cmdStateUpdateProgress(cwd, raw) {
}
function cmdStateAddDecision(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
const { phase, summary, summary_file, rationale, rationale_file } = options;
@@ -354,7 +387,7 @@ function cmdStateAddDecision(cwd, options, raw) {
}
function cmdStateAddBlocker(cwd, text, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
const blockerOptions = typeof text === 'object' && text !== null ? text : { text };
let blockerText = null;
@@ -387,7 +420,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
}
function cmdStateResolveBlocker(cwd, text, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
if (!text) { output({ error: 'text required' }, raw); return; }
@@ -419,7 +452,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
}
function cmdStateRecordSession(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
@@ -454,7 +487,7 @@ function cmdStateRecordSession(cwd, options, raw) {
}
function cmdStateSnapshot(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw);
@@ -576,7 +609,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
const info = getMilestoneInfo(cwd);
milestone = info.version;
milestoneName = info.name;
} catch {}
} catch { /* intentionally empty */ }
}
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
@@ -586,7 +619,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
if (cwd) {
try {
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
if (fs.existsSync(phasesDir)) {
const isDirInMilestone = getMilestonePhaseFilter(cwd);
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
@@ -611,7 +644,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
totalPlans = diskTotalPlans;
completedPlans = diskTotalSummaries;
}
} catch {}
} catch { /* intentionally empty */ }
}
let progressPercent = null;
@@ -664,7 +697,17 @@ function buildStateFrontmatter(bodyContent, cwd) {
}
function stripFrontmatter(content) {
return content.replace(/^---\n[\s\S]*?\n---\n*/, '');
// Strip ALL frontmatter blocks at the start of the file.
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
let result = content;
// eslint-disable-next-line no-constant-condition
while (true) {
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
if (stripped === result) break;
result = stripped;
}
return result;
}
function syncStateFrontmatter(content, cwd) {
@@ -677,14 +720,58 @@ function syncStateFrontmatter(content, cwd) {
/**
* Write STATE.md with synchronized YAML frontmatter.
* All STATE.md writes should use this instead of raw writeFileSync.
* Uses a simple lockfile to prevent parallel agents from overwriting
* each other's changes (race condition with read-modify-write cycle).
*/
function writeStateMd(statePath, content, cwd) {
const synced = syncStateFrontmatter(content, cwd);
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
const lockPath = statePath + '.lock';
const maxRetries = 10;
const retryDelay = 200; // ms
// Acquire lock (spin with backoff)
for (let i = 0; i < maxRetries; i++) {
try {
// O_EXCL fails if file already exists — atomic lock
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
fs.writeSync(fd, String(process.pid));
fs.closeSync(fd);
break;
} catch (err) {
if (err.code === 'EEXIST') {
// Check for stale lock (> 10s old)
try {
const stat = fs.statSync(lockPath);
if (Date.now() - stat.mtimeMs > 10000) {
fs.unlinkSync(lockPath);
continue; // retry immediately after clearing stale lock
}
} catch { /* lock was released between check — retry */ }
if (i === maxRetries - 1) {
// Last resort: write anyway rather than losing data
try { fs.unlinkSync(lockPath); } catch {}
break;
}
// Spin-wait with small jitter
const jitter = Math.floor(Math.random() * 50);
const start = Date.now();
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
continue;
}
break; // non-EEXIST error — proceed without lock
}
}
try {
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
} finally {
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
}
}
function cmdStateJson(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw, 'STATE.md not found');
return;
@@ -710,7 +797,7 @@ function cmdStateJson(cwd, raw) {
* Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text).
*/
function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw);
return;
@@ -778,9 +865,57 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
}
/**
* Write a WAITING.json signal file when GSD hits a decision point.
* External watchers (fswatch, polling, orchestrators) can detect this.
* File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists).
* Fixes #1034.
*/
function cmdSignalWaiting(cwd, type, question, options, phase, raw) {
const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : path.join(cwd, '.planning');
const waitingPath = path.join(gsdDir, 'WAITING.json');
const signal = {
status: 'waiting',
type: type || 'decision_point',
question: question || null,
options: options ? options.split('|').map(o => o.trim()) : [],
since: new Date().toISOString(),
phase: phase || null,
};
try {
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8');
output({ signaled: true, path: waitingPath }, raw, 'true');
} catch (e) {
output({ signaled: false, error: e.message }, raw, 'false');
}
}
/**
* Remove the WAITING.json signal file when user answers and agent resumes.
*/
function cmdSignalResume(cwd, raw) {
const paths = [
path.join(cwd, '.gsd', 'WAITING.json'),
path.join(cwd, '.planning', 'WAITING.json'),
];
let removed = false;
for (const p of paths) {
if (fs.existsSync(p)) {
try { fs.unlinkSync(p); removed = true; } catch {}
}
}
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
}
module.exports = {
stateExtractField,
stateReplaceField,
stateReplaceFieldWithFallback,
writeStateMd,
cmdStateLoad,
cmdStateGet,
@@ -796,4 +931,6 @@ module.exports = {
cmdStateSnapshot,
cmdStateJson,
cmdStateBeginPhase,
cmdSignalWaiting,
cmdSignalResume,
};

View File

@@ -0,0 +1,189 @@
/**
* UAT Audit — Cross-phase UAT/VERIFICATION scanner
*
* Reads all *-UAT.md and *-VERIFICATION.md files across all phases.
* Extracts non-passing items. Returns structured JSON for workflow consumption.
*/
const fs = require('fs');
const path = require('path');
const { output, error, getMilestonePhaseFilter } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
function cmdAuditUat(cwd, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
if (!fs.existsSync(phasesDir)) {
error('No .planning/phases directory found');
}
const isDirInMilestone = getMilestonePhaseFilter(cwd);
const results = [];
// Scan all phase directories
const dirs = fs.readdirSync(phasesDir, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name)
.filter(isDirInMilestone)
.sort();
for (const dir of dirs) {
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
const phaseDir = path.join(phasesDir, dir);
const files = fs.readdirSync(phaseDir);
// Process UAT files
for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
const items = parseUatItems(content);
if (items.length > 0) {
results.push({
phase: phaseNum,
phase_dir: dir,
file,
file_path: `.planning/phases/${dir}/${file}`,
type: 'uat',
status: (extractFrontmatter(content).status || 'unknown'),
items,
});
}
}
// Process VERIFICATION files
for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
const status = extractFrontmatter(content).status || 'unknown';
if (status === 'human_needed' || status === 'gaps_found') {
const items = parseVerificationItems(content, status);
if (items.length > 0) {
results.push({
phase: phaseNum,
phase_dir: dir,
file,
file_path: `.planning/phases/${dir}/${file}`,
type: 'verification',
status,
items,
});
}
}
}
}
// Compute summary
const summary = {
total_files: results.length,
total_items: results.reduce((sum, r) => sum + r.items.length, 0),
by_category: {},
by_phase: {},
};
for (const r of results) {
if (!summary.by_phase[r.phase]) summary.by_phase[r.phase] = 0;
for (const item of r.items) {
summary.by_phase[r.phase]++;
const cat = item.category || 'unknown';
summary.by_category[cat] = (summary.by_category[cat] || 0) + 1;
}
}
output({ results, summary }, raw);
}
function parseUatItems(content) {
const items = [];
// Match test blocks: ### N. Name\nexpected: ...\nresult: ...\n
const testPattern = /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)(?:\n(?:reported|reason|blocked_by):\s*[^\n]*)?/g;
let match;
while ((match = testPattern.exec(content)) !== null) {
const [, num, name, expected, result] = match;
if (result === 'pending' || result === 'skipped' || result === 'blocked') {
// Extract optional fields — limit to current test block (up to next ### or EOF)
const afterMatch = content.slice(match.index);
const nextHeading = afterMatch.indexOf('\n###', 1);
const blockText = nextHeading > 0 ? afterMatch.slice(0, nextHeading) : afterMatch;
const reasonMatch = blockText.match(/reason:\s*(.+)/);
const blockedByMatch = blockText.match(/blocked_by:\s*(.+)/);
const item = {
test: parseInt(num, 10),
name: name.trim(),
expected: expected.trim(),
result,
category: categorizeItem(result, reasonMatch?.[1], blockedByMatch?.[1]),
};
if (reasonMatch) item.reason = reasonMatch[1].trim();
if (blockedByMatch) item.blocked_by = blockedByMatch[1].trim();
items.push(item);
}
}
return items;
}
function parseVerificationItems(content, status) {
const items = [];
if (status === 'human_needed') {
// Extract from human_verification section — look for numbered items or table rows
const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i);
if (hvSection) {
const lines = hvSection[1].split('\n');
for (const line of lines) {
// Match table rows: | N | description | ... |
const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
// Match bullet items: - description
const bulletMatch = line.match(/^[-*]\s+(.+)/);
// Match numbered items: 1. description
const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
if (tableMatch) {
items.push({
test: parseInt(tableMatch[1], 10),
name: tableMatch[2].trim(),
result: 'human_needed',
category: 'human_uat',
});
} else if (numberedMatch) {
items.push({
test: parseInt(numberedMatch[1], 10),
name: numberedMatch[2].trim(),
result: 'human_needed',
category: 'human_uat',
});
} else if (bulletMatch && bulletMatch[1].length > 10) {
items.push({
name: bulletMatch[1].trim(),
result: 'human_needed',
category: 'human_uat',
});
}
}
}
}
// gaps_found items are already handled by plan-phase --gaps pipeline
return items;
}
function categorizeItem(result, reason, blockedBy) {
if (result === 'blocked' || blockedBy) {
if (blockedBy) {
if (/server/i.test(blockedBy)) return 'server_blocked';
if (/device|physical/i.test(blockedBy)) return 'device_needed';
if (/build|release|preview/i.test(blockedBy)) return 'build_needed';
if (/third.party|twilio|stripe/i.test(blockedBy)) return 'third_party';
}
return 'blocked';
}
if (result === 'skipped') {
if (reason) {
if (/server|not running|not available/i.test(reason)) return 'server_blocked';
if (/simulator|physical|device/i.test(reason)) return 'device_needed';
if (/build|release|preview/i.test(reason)) return 'build_needed';
}
return 'skipped_unresolved';
}
if (result === 'pending') return 'pending';
if (result === 'human_needed') return 'human_uat';
return 'unknown';
}
module.exports = { cmdAuditUat };

View File

@@ -5,7 +5,7 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, output, error } = require('./core.cjs');
const { safeReadFile, loadConfig, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error } = require('./core.cjs');
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
@@ -409,7 +409,7 @@ function cmdValidateConsistency(cwd, raw) {
}
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
const roadmapContent = stripShippedMilestones(roadmapContentRaw);
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
// Extract phases from ROADMAP (archived milestones already stripped)
const roadmapPhases = new Set();
@@ -428,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) {
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
if (dm) diskPhases.add(dm[1]);
}
} catch {}
} catch { /* intentionally empty */ }
// Check: phases in ROADMAP but not on disk
for (const p of roadmapPhases) {
@@ -445,15 +445,18 @@ function cmdValidateConsistency(cwd, raw) {
}
}
// Check: sequential phase numbers (integers only)
const integerPhases = [...diskPhases]
.filter(p => !p.includes('.'))
.map(p => parseInt(p, 10))
.sort((a, b) => a - b);
// Check: sequential phase numbers (integers only, skip in custom naming mode)
const config = loadConfig(cwd);
if (config.phase_naming !== 'custom') {
const integerPhases = [...diskPhases]
.filter(p => !p.includes('.'))
.map(p => parseInt(p, 10))
.sort((a, b) => a - b);
for (let i = 1; i < integerPhases.length; i++) {
if (integerPhases[i] !== integerPhases[i - 1] + 1) {
warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
for (let i = 1; i < integerPhases.length; i++) {
if (integerPhases[i] !== integerPhases[i - 1] + 1) {
warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
}
}
}
@@ -490,7 +493,7 @@ function cmdValidateConsistency(cwd, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// Check: frontmatter in plans has required fields
try {
@@ -510,7 +513,7 @@ function cmdValidateConsistency(cwd, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
const passed = errors.length === 0;
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
@@ -599,15 +602,19 @@ function cmdValidateHealth(cwd, options, raw) {
if (m) diskPhases.add(m[1]);
}
}
} catch {}
} catch { /* intentionally empty */ }
// Check for invalid references
for (const ref of phaseRefs) {
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
// Only warn if phases dir has any content (not just an empty project)
if (diskPhases.size > 0) {
addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /gsd:health --repair to regenerate STATE.md', true);
if (!repairs.includes('regenerateState')) repairs.push('regenerateState');
addIssue(
'warning',
'W002',
`STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`,
'Review STATE.md manually before changing it; /gsd:health --repair will not overwrite an existing STATE.md for phase mismatches'
);
}
}
}
@@ -641,7 +648,7 @@ function cmdValidateHealth(cwd, options, raw) {
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true);
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
}
} catch {}
} catch { /* intentionally empty */ }
}
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
@@ -652,7 +659,7 @@ function cmdValidateHealth(cwd, options, raw) {
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
}
}
} catch {}
} catch { /* intentionally empty */ }
// ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
try {
@@ -671,7 +678,7 @@ function cmdValidateHealth(cwd, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
try {
@@ -689,13 +696,13 @@ function cmdValidateHealth(cwd, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// ─── Check 8: Run existing consistency checks ─────────────────────────────
// Inline subset of cmdValidateConsistency
if (fs.existsSync(roadmapPath)) {
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
const roadmapContent = stripShippedMilestones(roadmapContentRaw);
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
const roadmapPhases = new Set();
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
let m;
@@ -712,7 +719,7 @@ function cmdValidateHealth(cwd, options, raw) {
if (dm) diskPhases.add(dm[1]);
}
}
} catch {}
} catch { /* intentionally empty */ }
// Phases in ROADMAP but not on disk
for (const p of roadmapPhases) {
@@ -746,6 +753,7 @@ function cmdValidateHealth(cwd, options, raw) {
branching_strategy: 'none',
phase_branch_template: 'gsd/phase-{phase}-{slug}',
milestone_branch_template: 'gsd/{milestone}-{slug}',
quick_branch_template: null,
workflow: {
research: true,
plan_check: true,

View File

@@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<task type="auto">
<name>Start dev server for verification</name>
<action>Run `npm run dev` in background, wait for "ready" message, capture port</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
<done>Dev server running at http://localhost:3000</done>
</task>
@@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<name>Deploy to Vercel</name>
<files>.vercel/, vercel.json</files>
<action>Run `vercel --yes` to deploy</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
<!-- If vercel returns "Error: Not authenticated", Claude creates checkpoint on the fly -->
@@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<task type="auto">
<name>Retry Vercel deployment</name>
<action>Run `vercel --yes` (now authenticated)</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
```
@@ -455,8 +455,8 @@ I'll verify: vercel whoami returns your account
npm run dev &
DEV_SERVER_PID=$!
# Wait for ready (max 30s)
timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done'
# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility
timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done'
```
**Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`).
@@ -489,7 +489,9 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
| Auth error | Create auth gate checkpoint |
| Network timeout | Retry with backoff, then checkpoint if persistent |
**Never present a checkpoint with broken verification environment.** If `curl localhost:3000` fails, don't ask user to "visit localhost:3000".
**Never present a checkpoint with broken verification environment.** If the local server isn't responding, don't ask user to "visit localhost:3000".
> **Cross-platform note:** Use `node -e "fetch('http://localhost:3000').then(r=>console.log(r.status))"` instead of `curl` for health checks. `curl` is broken on Windows MSYS/Git Bash due to SSL/path mangling issues.
```xml
<!-- WRONG: Checkpoint with broken environment -->
@@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Fix server startup issue</name>
<action>Investigate error, fix root cause, restart server</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify">
@@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Start dev server for auth testing</name>
<action>Run `npm run dev` in background, wait for ready signal</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
<done>Dev server running at http://localhost:3000</done>
</task>
@@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Start dev server</name>
<action>Run `npm run dev` in background</action>
<verify>curl localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify" gate="blocking">
@@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Deploy to Vercel</name>
<action>Run `vercel --yes`. Capture URL.</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
<task type="checkpoint:human-verify">

View File

@@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p
Each task gets its own commit immediately after completion.
> **Parallel agents:** When running as a parallel executor (spawned by execute-phase),
> use `--no-verify` on all commits to avoid pre-commit hook lock contention.
> The orchestrator validates hooks once after all agents complete.
```
{type}({phase}-{plan}): {task-name}
@@ -246,3 +250,46 @@ Each plan produces 2-4 commits (tasks + metadata). Clear, granular, bisectable.
- "Commit noise" irrelevant when consumer is Claude, not humans
</commit_strategy_rationale>
<sub_repos_support>
## Multi-Repo Workspace Support (sub_repos)
For workspaces with separate git repos (e.g., `backend/`, `frontend/`, `shared/`), GSD routes commits to each repo independently.
### Configuration
In `.planning/config.json`, list sub-repo directories under `planning.sub_repos`:
```json
{
"planning": {
"commit_docs": false,
"sub_repos": ["backend", "frontend", "shared"]
}
}
```
Set `commit_docs: false` so planning docs stay local and are not committed to any sub-repo.
### How It Works
1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk.
2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo).
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.
### Commit Routing
Instead of the standard `commit` command, use `commit-to-subrepo` when `sub_repos` is configured:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "feat(02-01): add user API" \
--files backend/src/api/users.ts backend/src/types/user.ts frontend/src/components/UserForm.tsx
```
This stages `src/api/users.ts` and `src/types/user.ts` in the `backend/` repo, and `src/components/UserForm.tsx` in the `frontend/` repo, then commits each independently with the same message.
Files that don't match any configured sub-repo are reported as unmatched.
</sub_repos_support>

View File

@@ -40,8 +40,26 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
**inherit** - Follow the current session model
- All agents resolve to `inherit`
- Best when you switch models interactively (for example OpenCode `/model`)
- **Required when using non-Anthropic providers** (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs
- Use when: you want GSD to follow your currently selected runtime model
## Using Non-Anthropic Models (OpenRouter, Local, etc.)
If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic provider, set the `inherit` profile to prevent GSD from calling Anthropic models for subagents:
```bash
# Via settings command
/gsd:settings
# → Select "Inherit" for model profile
# Or manually in .planning/config.json
{
"model_profile": "inherit"
}
```
Without `inherit`, GSD's default `balanced` profile spawns specific Anthropic models (`opus`, `sonnet`, `haiku`) for each agent type, which can result in additional API costs through your non-Anthropic provider.
## Resolution Logic
Orchestrators resolve model before spawning:

View File

@@ -11,7 +11,8 @@ Configuration options for `.planning/` directory behavior.
"git": {
"branching_strategy": "none",
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}"
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
}
```
@@ -22,6 +23,7 @@ Configuration options for `.planning/` directory behavior.
| `git.branching_strategy` | `"none"` | Git branching approach: `"none"`, `"phase"`, or `"milestone"` |
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy |
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy |
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
</config_schema>
<commit_docs_behavior>

View File

@@ -8,7 +8,7 @@ Template for `.planning/phases/XX-name/{phase_num}-UAT.md` — persistent UAT se
```markdown
---
status: testing | complete | diagnosed
status: testing | partial | complete | diagnosed
phase: XX-name
source: [list of SUMMARY.md files tested]
started: [ISO timestamp]
@@ -45,6 +45,12 @@ expected: [observable behavior]
result: skipped
reason: [why skipped]
### 5. [Test Name]
expected: [observable behavior]
result: blocked
blocked_by: server | physical-device | release-build | third-party | prior-phase
reason: [why blocked]
...
## Summary
@@ -54,6 +60,7 @@ passed: [N]
issues: [N]
pending: [N]
skipped: [N]
blocked: [N]
## Gaps
@@ -74,7 +81,7 @@ skipped: [N]
<section_rules>
**Frontmatter:**
- `status`: OVERWRITE - "testing" or "complete"
- `status`: OVERWRITE - "testing", "partial", or "complete"
- `phase`: IMMUTABLE - set on creation
- `source`: IMMUTABLE - SUMMARY files being tested
- `started`: IMMUTABLE - set on creation
@@ -87,9 +94,10 @@ skipped: [N]
**Tests:**
- Each test: OVERWRITE result field when user responds
- `result` values: [pending], pass, issue, skipped
- `result` values: [pending], pass, issue, skipped, blocked
- If issue: add `reported` (verbatim) and `severity` (inferred)
- If skipped: add `reason` if provided
- If blocked: add `blocked_by` (tag) and `reason` (if provided)
**Summary:**
- OVERWRITE counts after each response
@@ -156,6 +164,16 @@ skipped: [N]
- Commit file
- Present summary with next steps
**Partial completion:**
- status → "partial" (if pending, blocked, or unresolved skipped tests remain)
- Current Test → "[testing paused — {N} items outstanding]"
- Commit file
- Present summary with outstanding items highlighted
**Resuming partial session:**
- `/gsd:verify-work {phase}` picks up from first pending/blocked test
- When all items resolved, status advances to "complete"
**Resume after /clear:**
1. Read frontmatter → know phase and status
2. Read Current Test → know where we are

View File

@@ -2,8 +2,8 @@
Template for project-root `CLAUDE.md` — auto-generated by `gsd-tools generate-claude-md`.
Contains 5 marker-bounded sections. Each section is independently updatable.
The `generate-claude-md` subcommand manages 4 sections (project, stack, conventions, architecture).
Contains 6 marker-bounded sections. Each section is independently updatable.
The `generate-claude-md` subcommand manages 5 sections (project, stack, conventions, architecture, workflow enforcement).
The profile section is managed exclusively by `generate-claude-profile`.
---
@@ -66,6 +66,22 @@ Conventions not yet established. Will populate as patterns emerge during develop
Architecture not yet mapped. Follow existing patterns found in the codebase.
```
### Workflow Enforcement Section
```
<!-- GSD:workflow-start source:GSD defaults -->
## GSD Workflow Enforcement
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
Use these entry points:
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
- `/gsd:debug` for investigation and bug fixing
- `/gsd:execute-phase` for planned phase work
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
<!-- GSD:workflow-end -->
```
### Profile Section (Placeholder Only)
```
<!-- GSD:profile-start -->
@@ -88,7 +104,8 @@ CLAUDE.md file and no profile section exists yet.
2. **Stack** — Technology choices (what tools are used)
3. **Conventions** — Code patterns and rules (how code is written)
4. **Architecture** — System structure (how components fit together)
5. **Profile** — Developer behavioral preferences (how to interact)
5. **Workflow Enforcement** — Default GSD entry points for file-changing work
6. **Profile** — Developer behavioral preferences (how to interact)
## Marker Format

View File

@@ -10,7 +10,8 @@
},
"planning": {
"commit_docs": true,
"search_gitignored": false
"search_gitignored": false,
"sub_repos": []
},
"parallelization": {
"enabled": true,

View File

@@ -0,0 +1,63 @@
# Discussion Log Template
Template for `.planning/phases/XX-name/{phase_num}-DISCUSSION-LOG.md` — audit trail of discuss-phase Q&A sessions.
**Purpose:** Software audit trail for decision-making. Captures all options considered, not just the selected one. Separate from CONTEXT.md which is the implementation artifact consumed by downstream agents.
**NOT for LLM consumption.** This file should never be referenced in `<files_to_read>` blocks or agent prompts.
## Format
```markdown
# Phase [X]: [Name] - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** [ISO date]
**Phase:** [phase number]-[phase name]
**Areas discussed:** [comma-separated list]
---
## [Area 1 Name]
| Option | Description | Selected |
|--------|-------------|----------|
| [Option 1] | [Brief description] | |
| [Option 2] | [Brief description] | ✓ |
| [Option 3] | [Brief description] | |
**User's choice:** [Selected option or verbatim free-text response]
**Notes:** [Any clarifications or rationale provided during discussion]
---
## [Area 2 Name]
...
---
## Claude's Discretion
[Areas delegated to Claude's judgment — list what was deferred and why]
## Deferred Ideas
[Ideas mentioned but not in scope for this phase]
---
*Phase: XX-name*
*Discussion log generated: [date]*
```
## Rules
- Generated automatically at end of every discuss-phase session
- Includes ALL options considered, not just the selected one
- Includes user's freeform notes and clarifications
- Clearly marked as audit-only, not an implementation artifact
- Does NOT interfere with CONTEXT.md generation or downstream agent behavior
- Committed alongside CONTEXT.md in the same git commit

View File

@@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components.
<name>Task 2: Create User API endpoints</name>
<files>src/features/user/api.ts</files>
<action>GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model.</action>
<verify>curl tests pass for all endpoints</verify>
<verify>fetch tests pass for all endpoints</verify>
<done>All CRUD operations work</done>
</task>
</tasks>
@@ -407,7 +407,7 @@ Output: Working dashboard component.
<task type="auto">
<name>Start dev server</name>
<action>Run `npm run dev` in background, wait for ready</action>
<verify>curl localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify" gate="blocking">

View File

@@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform
<evolution>
PROJECT.md evolves throughout the project lifecycle.
These rules are embedded in the generated PROJECT.md (## Evolution section)
and implemented by workflows/transition.md and workflows/complete-milestone.md.
**After each phase transition:**
1. Requirements invalidated? → Move to Out of Scope with reason

View File

@@ -0,0 +1,109 @@
<purpose>
Cross-phase audit of all UAT and verification files. Finds every outstanding item (pending, skipped, blocked, human_needed), optionally verifies against the codebase to detect stale docs, and produces a prioritized human test plan.
</purpose>
<process>
<step name="initialize">
Run the CLI audit:
```bash
AUDIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw)
```
Parse JSON for `results` array and `summary` object.
If `summary.total_items` is 0:
```
## All Clear
No outstanding UAT or verification items found across all phases.
All tests are passing, resolved, or diagnosed with fix plans.
```
Stop here.
</step>
<step name="categorize">
Group items by what's actionable NOW vs. what needs prerequisites:
**Testable Now** (no external dependencies):
- `pending` — tests never run
- `human_uat` — human verification items
- `skipped_unresolved` — skipped without clear blocking reason
**Needs Prerequisites:**
- `server_blocked` — needs external server running
- `device_needed` — needs physical device (not simulator)
- `build_needed` — needs release/preview build
- `third_party` — needs external service configuration
For each item in "Testable Now", use Grep/Read to check if the underlying feature still exists in the codebase:
- If the test references a component/function that no longer exists → mark as `stale`
- If the test references code that has been significantly rewritten → mark as `needs_update`
- Otherwise → mark as `active`
</step>
<step name="present">
Present the audit report:
```
## UAT Audit Report
**{total_items} outstanding items across {total_files} files in {phase_count} phases**
### Testable Now ({count})
| # | Phase | Test | Description | Status |
|---|-------|------|-------------|--------|
| 1 | {phase} | {test_name} | {expected} | {active/stale/needs_update} |
...
### Needs Prerequisites ({count})
| # | Phase | Test | Blocked By | Description |
|---|-------|------|------------|-------------|
| 1 | {phase} | {test_name} | {category} | {expected} |
...
### Stale (can be closed) ({count})
| # | Phase | Test | Why Stale |
|---|-------|------|-----------|
| 1 | {phase} | {test_name} | {reason} |
...
---
## Recommended Actions
1. **Close stale items:** `/gsd:verify-work {phase}` — mark stale tests as resolved
2. **Run active tests:** Human UAT test plan below
3. **When prerequisites met:** Retest blocked items with `/gsd:verify-work {phase}`
```
</step>
<step name="test_plan">
Generate a human UAT test plan for "Testable Now" + "active" items only:
Group by what can be tested together (same screen, same feature, same prerequisite):
```
## Human UAT Test Plan
### Group 1: {category — e.g., "Billing Flow"}
Prerequisites: {what needs to be running/configured}
1. **{Test name}** (Phase {N})
- Navigate to: {where}
- Do: {action}
- Expected: {expected behavior}
2. **{Test name}** (Phase {N})
...
### Group 2: {category}
...
```
</step>
</process>

View File

@@ -110,6 +110,18 @@ Phase: "API documentation"
1. Retry the question once with the same parameters
2. If still empty, present the options as a plain-text numbered list and ask the user to type their choice number
Never proceed with an empty answer.
**Text mode (`workflow.text_mode: true` in config or `--text` flag):**
When text mode is active, **do not use AskUserQuestion at all**. Instead, present every
question as a plain-text numbered list and ask the user to type their choice number.
This is required for Claude Code remote sessions (`/rc` mode) where the Claude App
cannot forward TUI menu selections back to the host.
Enable text mode:
- Per-session: pass `--text` flag to any command (e.g., `/gsd:discuss-phase --text`)
- Per-project: `gsd-tools config-set workflow.text_mode true`
Text mode applies to ALL workflows in the session, not just discuss-phase.
</answer_validation>
<process>
@@ -242,6 +254,47 @@ Structure the extracted information:
**If no prior context exists:** Continue without — this is expected for early phases.
</step>
<step name="cross_reference_todos">
Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed.
**Load and match todos:**
```bash
TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}")
```
Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`).
**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown.
**If matches found:**
Present matched todos to the user. Show each match with its title, area, and why it matched:
```
📋 Found {N} pending todo(s) that may be relevant to Phase {X}:
{For each match:}
- **{title}** (area: {area}, relevance: {score}) — matched on {reasons}
```
Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope:
```
Which of these todos should be folded into Phase {X} scope?
(Select any that apply, or none to skip)
```
**For selected (folded) todos:**
- Store internally as `<folded_todos>` for inclusion in CONTEXT.md `<decisions>` section
- These become additional scope items that downstream agents (researcher, planner) will see
**For unselected (reviewed but not folded) todos:**
- Store internally as `<reviewed_todos>` for inclusion in CONTEXT.md `<deferred>` section
- This prevents future phases from re-surfacing the same todos as "missed"
**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection.
</step>
<step name="scout_codebase">
Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session.
@@ -404,8 +457,58 @@ Continue to discuss_areas with selected areas.
<step name="discuss_areas">
For each selected area, conduct a focused discussion loop.
**Research-before-questions mode:** Check if `research_questions` is enabled in config (from init context or `.planning/config.json`). When enabled, before presenting questions for each area:
1. Do a brief web search for best practices related to the area topic
2. Summarize the top findings in 2-3 bullet points
3. Present the research alongside the question so the user can make a more informed decision
Example with research enabled:
```
Let's talk about [Authentication Strategy].
📊 Best practices research:
• OAuth 2.0 + PKCE is the current standard for SPAs (replaces implicit flow)
• Session tokens with httpOnly cookies preferred over localStorage for XSS protection
• Consider passkey/WebAuthn support — adoption is accelerating in 2025-2026
With that context: How should users authenticate?
```
When disabled (default), skip the research and present questions directly as before.
**Text mode support:** Parse optional `--text` from `$ARGUMENTS`.
- Accept `--text` flag OR read `workflow.text_mode` from config (from init context)
- When active, replace ALL `AskUserQuestion` calls with plain-text numbered lists
- User types a number to select, or types free text for "Other"
- This is required for Claude Code remote sessions (`/rc` mode) where TUI menus
don't work through the Claude App
**Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`.
- Accept `--batch`, `--batch=N`, or `--batch N`
**Analyze mode support:** Parse optional `--analyze` from `$ARGUMENTS`.
When `--analyze` is active, before presenting each question (or question group in batch mode), provide a brief **trade-off analysis** for the decision:
- 2-3 options with pros/cons based on codebase context and common patterns
- A recommended approach with reasoning
- Known pitfalls or constraints from prior phases
Example with `--analyze`:
```
**Trade-off analysis: Authentication strategy**
| Approach | Pros | Cons |
|----------|------|------|
| Session cookies | Simple, httpOnly prevents XSS | Requires CSRF protection, sticky sessions |
| JWT (stateless) | Scalable, no server state | Token size, revocation complexity |
| OAuth 2.0 + PKCE | Industry standard for SPAs | More setup, redirect flow UX |
💡 Recommended: OAuth 2.0 + PKCE — your app has social login in requirements (REQ-04) and this aligns with the existing NextAuth setup in `src/lib/auth.ts`.
How should users authenticate?
```
This gives the user context to make informed decisions without extra prompting. When `--analyze` is absent, present questions directly as before.
- Accept `--batch`, `--batch=N`, or `--batch N`
- Default to 4 questions per batch when no number is provided
- Clamp explicit sizes to 2-5 so a batch stays answerable
- If `--batch` is absent, keep the existing one-question-at-a-time flow
@@ -500,11 +603,23 @@ Back to [current area]: [return to current question]"
```
Track deferred ideas internally.
**Track discussion log data internally:**
For each question asked, accumulate:
- Area name
- All options presented (label + description)
- Which option the user selected (or their free-text response)
- Any follow-up notes or clarifications the user provided
This data is used to generate DISCUSSION-LOG.md in the `write_context` step.
</step>
<step name="write_context">
Create CONTEXT.md capturing decisions made.
**Also generate DISCUSSION-LOG.md** — a full audit trail of the discuss-phase Q&A.
This file is for human reference only (software audits, compliance reviews). It is NOT
consumed by downstream agents (researcher, planner, executor).
**Find or create phase directory:**
Use values from init: `phase_dir`, `phase_slug`, `padded_phase`.
@@ -544,6 +659,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
### Claude's Discretion
[Areas where user said "you decide" — note that Claude has flexibility here]
### Folded Todos
[If any todos were folded into scope from the cross_reference_todos step, list them here.
Each entry should include the todo title, original problem, and how it fits this phase's scope.
If no todos were folded: omit this subsection entirely.]
</decisions>
<canonical_refs>
@@ -595,6 +715,12 @@ Every entry needs a full relative path — not just a name.]
[Ideas that came up but belong in other phases. Don't lose them.]
### Reviewed Todos (not folded)
[If any todos were reviewed in cross_reference_todos but not folded into scope,
list them here so future phases know they were considered.
Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.)
If no reviewed-but-deferred todos: omit this subsection entirely.]
[If none: "None — discussion stayed within phase scope"]
</deferred>
@@ -648,10 +774,54 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md
</step>
<step name="git_commit">
Commit phase context (uses `commit_docs` from init internally):
**Write DISCUSSION-LOG.md before committing:**
**File location:** `${phase_dir}/${padded_phase}-DISCUSSION-LOG.md`
```markdown
# Phase [X]: [Name] - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** [ISO date]
**Phase:** [phase number]-[phase name]
**Areas discussed:** [comma-separated list]
---
[For each gray area discussed:]
## [Area Name]
| Option | Description | Selected |
|--------|-------------|----------|
| [Option 1] | [Description from AskUserQuestion] | |
| [Option 2] | [Description] | ✓ |
| [Option 3] | [Description] | |
**User's choice:** [Selected option or free-text response]
**Notes:** [Any clarifications, follow-up context, or rationale the user provided]
---
[Repeat for each area]
## Claude's Discretion
[List areas where user said "you decide" or deferred to Claude]
## Deferred Ideas
[Ideas mentioned during discussion that were noted for future phases]
```
Write file.
Commit phase context and discussion log:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
```
Confirm: "Committed: docs(${padded_phase}): capture phase context"

View File

@@ -6,10 +6,45 @@ Execute all plans in a phase using wave-based parallel execution. Orchestrator s
Orchestrator coordinates, not executes. Each subagent loads the full execute-plan context. Orchestrator: discover plans → analyze deps → group waves → spawn agents → handle checkpoints → collect results.
</core_principle>
<runtime_compatibility>
**Subagent spawning is runtime-specific:**
- **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result
- **Copilot:** Subagent spawning does not reliably return completion signals. **Default to
sequential inline execution**: read and follow execute-plan.md directly for each plan
instead of spawning parallel agents. Only attempt parallel spawning if the user
explicitly requests it — and in that case, rely on the spot-check fallback in step 3
to detect completion.
- **Other runtimes (Gemini, Codex, OpenCode):** If Task/subagent API is unavailable, use sequential
inline execution as the fallback.
**Fallback rule:** If a spawned agent completes its work (commits visible, SUMMARY.md exists) but
the orchestrator never receives the completion signal, treat it as successful based on spot-checks
and continue to the next wave/plan. Never block indefinitely waiting for a signal — always verify
via filesystem and git state.
</runtime_compatibility>
<required_reading>
Read STATE.md before any operation to load project context.
</required_reading>
<available_agent_types>
These are the valid GSD subagent types registered in .claude/agents/ (or equivalent for your runtime).
Always use the exact name from this list — do not fall back to 'general-purpose' or other built-in types:
- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md
- gsd-verifier — Verifies phase completion, checks quality gates
- gsd-planner — Creates detailed plans from phase scope
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-plan-checker — Reviews plan quality before execution
- gsd-debugger — Diagnoses and fixes issues
- gsd-codebase-mapper — Maps project structure and dependencies
- gsd-integration-checker — Checks cross-phase integration
- gsd-nyquist-auditor — Validates verification coverage
- gsd-ui-researcher — Researches UI/UX approaches
- gsd-ui-checker — Reviews UI implementation quality
- gsd-ui-auditor — Audits UI against design requirements
</available_agent_types>
<process>
<step name="initialize" priority="first">
@@ -28,6 +63,14 @@ Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelizat
When `parallelization` is false, plans within a wave execute sequentially.
**Runtime detection for Copilot:**
Check if the current runtime is Copilot by testing for the `@gsd-executor` agent pattern
or absence of the `Task()` subagent API. If running under Copilot, force sequential inline
execution regardless of the `parallelization` setting — Copilot's subagent completion
signals are unreliable (see `<runtime_compatibility>`). Set `COPILOT_SEQUENTIAL=true`
internally and skip the `execute_waves` step in favor of `check_interactive_mode`'s
inline path for each plan.
**REQUIRED — Sync chain flag with intent.** If user invoked manually (no `--auto`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This prevents stale `_auto_chain_active: true` from causing unwanted auto-advance. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference). You MUST execute this bash block before any config reads:
```bash
# REQUIRED: prevents stale auto-chain from previous --auto runs
@@ -37,6 +80,54 @@ fi
```
</step>
<step name="check_interactive_mode">
**Parse `--interactive` flag from $ARGUMENTS.**
**If `--interactive` flag present:** Switch to interactive execution mode.
Interactive mode executes plans sequentially **inline** (no subagent spawning) with user
checkpoints between tasks. The user can review, modify, or redirect work at any point.
**Interactive execution flow:**
1. Load plan inventory as normal (discover_and_group_plans)
2. For each plan (sequentially, ignoring wave grouping):
a. **Present the plan to the user:**
```
## Plan {plan_id}: {plan_name}
Objective: {from plan file}
Tasks: {task_count}
Options:
- Execute (proceed with all tasks)
- Review first (show task breakdown before starting)
- Skip (move to next plan)
- Stop (end execution, save progress)
```
b. **If "Review first":** Read and display the full plan file. Ask again: Execute, Modify, Skip.
c. **If "Execute":** Read and follow `~/.claude/get-shit-done/workflows/execute-plan.md` **inline**
(do NOT spawn a subagent). Execute tasks one at a time.
d. **After each task:** Pause briefly. If the user intervenes (types anything), stop and address
their feedback before continuing. Otherwise proceed to next task.
e. **After plan complete:** Show results, commit, create SUMMARY.md, then present next plan.
3. After all plans: proceed to verification (same as normal mode).
**Benefits of interactive mode:**
- No subagent overhead — dramatically lower token usage
- User catches mistakes early — saves costly verification cycles
- Maintains GSD's planning/tracking structure
- Best for: small phases, bug fixes, verification gaps, learning GSD
**Skip to handle_branching step** (interactive plans execute inline after grouping).
</step>
<step name="handle_branching">
Check `branching_strategy` from init:
@@ -111,8 +202,9 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
2. **Spawn executor agents:**
Pass paths only — executors read files themselves with their fresh 200k context.
This keeps orchestrator context lean (~10-15%).
Pass paths only — executors read files themselves with their fresh context window.
For 200k models, this keeps orchestrator context lean (~10-15%).
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
```
Task(
@@ -124,6 +216,14 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md.
</objective>
<parallel_execution>
You are running as a PARALLEL executor agent. Use --no-verify on all git
commits to avoid pre-commit hook contention with other agents. The
orchestrator validates hooks once after all agents complete.
For gsd-tools commits: add --no-verify flag.
For direct git commits: use git commit --no-verify -m "..."
</parallel_execution>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
@@ -134,12 +234,20 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
<files_to_read>
Read these files at execution start using the Read tool:
- {phase_dir}/{plan_file} (Plan)
- .planning/PROJECT.md (Project context — core value, requirements, evolution rules)
- .planning/STATE.md (State)
- .planning/config.json (Config, if exists)
- ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions)
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
</files_to_read>
<mcp_tools>
If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7,
or other MCP servers), prefer those tools over Grep/Glob for code navigation when available.
MCP tools often save significant tokens by providing structured code indexes.
Check tool availability first — if MCP tools are not accessible, fall back to Grep/Glob.
</mcp_tools>
<success_criteria>
- [ ] All tasks executed
- [ ] Each task committed individually
@@ -153,7 +261,39 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
3. **Wait for all agents in wave to complete.**
4. **Report completion — spot-check claims first:**
**Completion signal fallback (Copilot and runtimes where Task() may not return):**
If a spawned agent does not return a completion signal but appears to have finished
its work, do NOT block indefinitely. Instead, verify completion via spot-checks:
```bash
# For each plan in this wave, check if the executor finished:
SUMMARY_EXISTS=$(test -f "{phase_dir}/{plan_number}-{plan_padded}-SUMMARY.md" && echo "true" || echo "false")
COMMITS_FOUND=$(git log --oneline --all --grep="{phase_number}-{plan_padded}" --since="1 hour ago" | head -1)
```
**If SUMMARY.md exists AND commits are found:** The agent completed successfully —
treat as done and proceed to step 4. Log: `"✓ {Plan ID} completed (verified via spot-check — completion signal not received)"`
**If SUMMARY.md does NOT exist after a reasonable wait:** The agent may still be
running or may have failed silently. Check `git log --oneline -5` for recent
activity. If commits are still appearing, wait longer. If no activity, report
the plan as failed and route to the failure handler in step 5.
**This fallback applies automatically to all runtimes.** Claude Code's Task() normally
returns synchronously, but the fallback ensures resilience if it doesn't.
4. **Post-wave hook validation (parallel mode only):**
When agents committed with `--no-verify`, run pre-commit hooks once after the wave:
```bash
# Run project's pre-commit hooks on the current state
git diff --cached --quiet || git stash # stash any unstaged changes
git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing"
```
If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?"
5. **Report completion — spot-check claims first:**
For each SUMMARY.md:
- Verify first 2 files from `key-files.created` exist on disk
@@ -328,6 +468,67 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PARENT
```
</step>
<step name="regression_gate">
Run prior phases' test suites to catch cross-phase regressions BEFORE verification.
**Skip if:** This is the first phase (no prior phases), or no prior VERIFICATION.md files exist.
**Step 1: Discover prior phases' test files**
```bash
# Find all VERIFICATION.md files from prior phases in current milestone
PRIOR_VERIFICATIONS=$(find .planning/phases/ -name "*-VERIFICATION.md" ! -path "*${PHASE_NUMBER}*" 2>/dev/null)
```
**Step 2: Extract test file lists from prior verifications**
For each VERIFICATION.md found, look for test file references:
- Lines containing `test`, `spec`, or `__tests__` paths
- The "Test Suite" or "Automated Checks" section
- File patterns from `key-files.created` in corresponding SUMMARY.md files that match `*.test.*` or `*.spec.*`
Collect all unique test file paths into `REGRESSION_FILES`.
**Step 3: Run regression tests (if any found)**
```bash
# Detect test runner and run prior phase tests
if [ -f "package.json" ]; then
# Node.js — use project's test runner
npx jest ${REGRESSION_FILES} --passWithNoTests --no-coverage -q 2>&1 || npx vitest run ${REGRESSION_FILES} 2>&1
elif [ -f "Cargo.toml" ]; then
cargo test 2>&1
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
python -m pytest ${REGRESSION_FILES} -q --tb=short 2>&1
fi
```
**Step 4: Report results**
If all tests pass:
```
✓ Regression gate: {N} prior-phase test files passed — no regressions detected
```
→ Proceed to verify_phase_goal
If any tests fail:
```
## ⚠ Cross-Phase Regression Detected
Phase {X} execution may have broken functionality from prior phases.
| Test File | Phase | Status | Detail |
|-----------|-------|--------|--------|
| {file} | {origin_phase} | FAILED | {first_failure_line} |
Options:
1. Fix regressions before verification (recommended)
2. Continue to verification anyway (regressions will compound)
3. Abort phase — roll back and re-plan
```
Use AskUserQuestion to present the options.
</step>
<step name="verify_phase_goal">
Verify phase achieved its GOAL, not just completed tasks.
@@ -357,6 +558,51 @@ grep "^status:" "$PHASE_DIR"/*-VERIFICATION.md | cut -d: -f2 | tr -d ' '
| `gaps_found` | Present gap summary, offer `/gsd:plan-phase {phase} --gaps` |
**If human_needed:**
**Step A: Persist human verification items as UAT file.**
Create `{phase_dir}/{phase_num}-HUMAN-UAT.md` using UAT template format:
```markdown
---
status: partial
phase: {phase_num}-{phase_name}
source: [{phase_num}-VERIFICATION.md]
started: [now ISO]
updated: [now ISO]
---
## Current Test
[awaiting human testing]
## Tests
{For each human_verification item from VERIFICATION.md:}
### {N}. {item description}
expected: {expected behavior from VERIFICATION.md}
result: [pending]
## Summary
total: {count}
passed: 0
issues: 0
pending: {count}
skipped: 0
blocked: 0
## Gaps
```
Commit the file:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "test({phase_num}): persist human verification items as UAT" --files "{phase_dir}/{phase_num}-HUMAN-UAT.md"
```
**Step B: Present to user:**
```
## ✓ Phase {X}: {Name} — Human Verification Required
@@ -364,9 +610,15 @@ All automated checks passed. {N} items need human testing:
{From VERIFICATION.md human_verification section}
Items saved to `{phase_num}-HUMAN-UAT.md` — they will appear in `/gsd:progress` and `/gsd:audit-uat`.
"approved" → continue | Report issues → gap closure
```
**If user says "approved":** Proceed to `update_roadmap`. The HUMAN-UAT.md file persists with `status: partial` and will surface in future progress checks until the user runs `/gsd:verify-work` on it.
**If user reports issues:** Proceed to gap closure as currently implemented.
**If gaps_found:**
```
## ⚠ Phase {X}: {Name} — Gaps Found
@@ -404,14 +656,46 @@ The CLI handles:
- Updating plan count to final
- Advancing STATE.md to next phase
- Updating REQUIREMENTS.md traceability
- Scanning for verification debt (returns `warnings` array)
Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`.
Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`, `warnings`, `has_warnings`.
**If has_warnings is true:**
```
## Phase {X} marked complete with {N} warnings:
{list each warning}
These items are tracked and will appear in `/gsd:progress` and `/gsd:audit-uat`.
```
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): complete phase execution" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md
```
</step>
<step name="update_project_md">
**Evolve PROJECT.md to reflect phase completion (prevents planning document drift — #956):**
PROJECT.md tracks validated requirements, decisions, and current state. Without this step,
PROJECT.md falls behind silently over multiple phases.
1. Read `.planning/PROJECT.md`
2. If the file exists and has a `## Validated Requirements` or `## Requirements` section:
- Move any requirements validated by this phase from Active → Validated
- Add a brief note: `Validated in Phase {X}: {Name}`
3. If the file has a `## Current State` or similar section:
- Update it to reflect this phase's completion (e.g., "Phase {X} complete — {one-liner}")
4. Update the `Last updated:` footer to today's date
5. Commit the change:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md
```
**Skip this step if** `.planning/PROJECT.md` does not exist.
</step>
<step name="offer_next">
**Exception:** If `gaps_found`, the `verify_phase_goal` step already presents the gap-closure path (`/gsd:plan-phase {X} --gaps`). No additional routing needed — skip auto-advance.
@@ -465,6 +749,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
**STOP. Do not auto-advance. Do not execute transition. Do not plan next phase. Present options to the user and wait.**
**IMPORTANT: There is NO `/gsd:transition` command. Never suggest it. The transition workflow is internal only.**
```
## ✓ Phase {X}: {Name} Complete
@@ -473,12 +759,20 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
/gsd:plan-phase {next} — plan next phase
/gsd:execute-phase {next} — execute next phase
```
Only suggest the commands listed above. Do not invent or hallucinate command names.
</step>
</process>
<context_efficiency>
Orchestrator: ~10-15% context. Subagents: fresh 200k each. No polling (Task blocks). No context bleed.
Orchestrator: ~10-15% context for 200k windows, can use more for 1M+ windows.
Subagents: fresh context each (200k-1M depending on model). No polling (Task blocks). No context bleed.
For 1M+ context models, consider:
- Passing richer context (code snippets, dependency outputs) directly to executors instead of just file paths
- Running small phases (≤3 plans, no dependencies) inline without subagent spawning overhead
- Relaxing /clear recommendations — context rot onset is much further out with 5x window
</context_efficiency>
<failure_handling>

View File

@@ -19,7 +19,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.
If `.planning/` missing: error.
</step>
@@ -135,7 +135,8 @@ If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness
Deviations are normal — handle via rules below.
1. Read @context files from prompt
2. Per task:
2. **MCP tools:** If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch for code navigation), prefer them over Grep/Glob when available. Fall back to Grep/Glob if MCP tools are not accessible.
3. Per task:
- **MANDATORY read_first gate:** If the task has a `<read_first>` field, you MUST read every listed file BEFORE making any edits. This is not optional. Do not skip files because you "already know" what's in them — read them. The read_first files establish ground truth for the task.
- `type="auto"`: if `tdd="true"` → TDD execution. Implement with deviation rules + auth gates. Verify done criteria. Commit (see task_commit). Track hash for Summary.
- `type="checkpoint:*"`: STOP → checkpoint_protocol → wait for user → continue only after confirmation.
@@ -233,6 +234,10 @@ See `~/.claude/get-shit-done/references/tdd.md` for structure.
Your commits may trigger pre-commit hooks. Auto-fix hooks handle themselves transparently — files get fixed and re-staged automatically.
**If running as a parallel executor agent (spawned by execute-phase):**
Use `--no-verify` on all commits. Pre-commit hooks cause build lock contention when multiple agents commit simultaneously (e.g., cargo lock fights in Rust projects). The orchestrator validates once after all agents complete.
**If running as the sole executor (sequential mode):**
If a commit is BLOCKED by a hook:
1. The `git commit` command fails with hook error output
@@ -240,9 +245,7 @@ If a commit is BLOCKED by a hook:
3. Fix the issue (type error, lint violation, secret leak, etc.)
4. `git add` the fixed files
5. Retry the commit
6. Do NOT use `--no-verify`
This is normal and expected. Budget 1-2 retry cycles per commit.
6. Budget 1-2 retry cycles per commit
</precommit_failure_handling>
<task_commit>
@@ -273,6 +276,20 @@ git add src/types/user.ts
**4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.
<sub_repos_commit_flow>
**Sub-repos mode:** If `sub_repos` is configured (non-empty array from init context), use `commit-to-subrepo` instead of standard git commit. This routes files to their correct sub-repo based on path prefix.
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {description}" --files file1 file2 ...
```
The command groups files by sub-repo prefix and commits atomically to each. Returns JSON: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`.
Record hashes from each repo in the response for SUMMARY tracking.
**If `sub_repos` is empty or not set:** Use standard git commit flow below.
</sub_repos_commit_flow>
**5. Record hash:**
```bash
TASK_COMMIT=$(git rev-parse --short HEAD)
@@ -370,7 +387,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not "
Include: duration, start/end times, task count, file count.
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition".
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step".
</step>
<step name="update_current_position">

View File

@@ -0,0 +1,105 @@
<purpose>
Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning,
no research, no plan checking. Just: understand → do → commit → log.
For tasks like: fix a typo, update a config value, add a missing import, rename a
variable, commit uncommitted work, add a .gitignore entry, bump a version number.
Use /gsd:quick for anything that needs multi-step planning or research.
</purpose>
<process>
<step name="parse_task">
Parse `$ARGUMENTS` for the task description.
If empty, ask:
```
What's the quick fix? (one sentence)
```
Store as `$TASK`.
</step>
<step name="scope_check">
**Before doing anything, verify this is actually trivial.**
A task is trivial if it can be completed in:
- ≤ 3 file edits
- ≤ 1 minute of work
- No new dependencies or architecture changes
- No research needed
If the task seems non-trivial (multi-file refactor, new feature, needs research),
say:
```
This looks like it needs planning. Use /gsd:quick instead:
/gsd:quick "{task description}"
```
And stop.
</step>
<step name="execute_inline">
Do the work directly:
1. Read the relevant file(s)
2. Make the change(s)
3. Verify the change works (run existing tests if applicable, or do a quick sanity check)
**No PLAN.md.** Just do it.
</step>
<step name="commit">
Commit the change atomically:
```bash
git add -A
git commit -m "fix: {concise description of what changed}"
```
Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` as appropriate.
</step>
<step name="log_to_state">
If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table.
If the table doesn't exist, skip this step silently.
```bash
# Check if STATE.md has quick tasks table
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
# Append entry — workflow handles the format
echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md
fi
```
</step>
<step name="done">
Report completion:
```
✅ Done: {what was changed}
Commit: {short hash}
Files: {list of changed files}
```
No next-step suggestions. No workflow routing. Just done.
</step>
</process>
<guardrails>
- NEVER spawn a Task/subagent — this runs inline
- NEVER create PLAN.md or SUMMARY.md files
- NEVER run research or plan-checking
- If the task takes more than 3 file edits, STOP and redirect to /gsd:quick
- If you're unsure how to implement it, STOP and redirect to /gsd:quick
</guardrails>
<success_criteria>
- [ ] Task completed in current context (no subagents)
- [ ] Atomic git commit with conventional message
- [ ] STATE.md updated if it exists
- [ ] Total operation under 2 minutes wall time
</success_criteria>

View File

@@ -72,8 +72,8 @@ Errors: N | Warnings: N | Info: N
```
## Warnings
- [W001] STATE.md references phase 5, but only phases 1-3 exist
Fix: Run /gsd:health --repair to regenerate
- [W002] STATE.md references phase 5, but only phases 1-3 exist
Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md
- [W005] Phase directory "1-setup" doesn't follow NN-name format
Fix: Rename to match pattern (e.g., 01-setup)
@@ -130,7 +130,7 @@ Report final status.
| E004 | error | STATE.md not found | Yes |
| E005 | error | config.json parse error | Yes |
| W001 | warning | PROJECT.md missing required section | No |
| W002 | warning | STATE.md references invalid phase | Yes |
| W002 | warning | STATE.md references invalid phase | No |
| W003 | warning | config.json not found | Yes |
| W004 | warning | config.json invalid field value | No |
| W005 | warning | Phase directory naming mismatch | No |
@@ -148,7 +148,7 @@ Report final status.
|--------|--------|------|
| createConfig | Create config.json with defaults | None |
| resetConfig | Delete + recreate config.json | Loses custom settings |
| regenerateState | Create STATE.md from ROADMAP structure | Loses session history |
| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history |
| addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default |
**Not repairable (too risky):**
@@ -157,3 +157,25 @@ Report final status.
- Orphaned plan cleanup
</repair_actions>
<stale_task_cleanup>
**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze.
These are left behind when subagents are force-killed and consume disk space.
When `--repair` is active, detect and clean up:
```bash
# Check for stale task directories (older than 24 hours)
TASKS_DIR="$HOME/.claude/tasks"
if [ -d "$TASKS_DIR" ]; then
STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l)
if [ "$STALE_COUNT" -gt 0 ]; then
echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/"
echo " These are leftover from crashed subagent sessions."
echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)"
fi
fi
```
Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)`
</stale_task_cleanup>

View File

@@ -151,6 +151,21 @@ Usage: `/gsd:quick`
Usage: `/gsd:quick --research --full`
Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md`
---
**`/gsd:fast [description]`**
Execute a trivial task inline — no subagents, no planning files, no overhead.
For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md.
- No PLAN.md or SUMMARY.md created
- No subagent spawned (runs inline)
- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial
- Atomic commit with conventional message
Usage: `/gsd:fast "fix the typo in README"`
Usage: `/gsd:fast "add .env to gitignore"`
### Roadmap Management
**`/gsd:add-phase <description>`**
@@ -192,10 +207,12 @@ Start a new milestone through unified flow.
- Optional domain research (spawns 4 parallel researcher agents)
- Requirements definition with scoping
- Roadmap creation with phase breakdown
- Optional `--reset-phase-numbers` flag restarts numbering at Phase 1 and archives old phase dirs first for safety
Mirrors `/gsd:new-project` flow for brownfield projects (existing PROJECT.md).
Usage: `/gsd:new-milestone "v2.0 Features"`
Usage: `/gsd:new-milestone --reset-phase-numbers "v2.0 Features"`
**`/gsd:complete-milestone <version>`**
Archive completed milestone and prepare for next version.
@@ -308,6 +325,65 @@ Validate built features through conversational UAT.
Usage: `/gsd:verify-work 3`
### Ship Work
**`/gsd:ship [phase]`**
Create a PR from completed phase work with an auto-generated body.
- Pushes branch to remote
- Creates PR with summary from SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md
- Optionally requests code review
- Updates STATE.md with shipping status
Prerequisites: Phase verified, `gh` CLI installed and authenticated.
Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft`
---
**`/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`**
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
- Detects available CLIs (gemini, claude, codex)
- Each CLI reviews plans independently with the same structured prompt
- Produces REVIEWS.md with per-reviewer feedback and consensus summary
- Feed reviews back into planning: `/gsd:plan-phase N --reviews`
Usage: `/gsd:review --phase 3 --all`
---
**`/gsd:pr-branch [target]`**
Create a clean branch for pull requests by filtering out .planning/ commits.
- Classifies commits: code-only (include), planning-only (exclude), mixed (include sans .planning/)
- Cherry-picks code commits onto a clean branch
- Reviewers see only code changes, no GSD artifacts
Usage: `/gsd:pr-branch` or `/gsd:pr-branch main`
---
**`/gsd:plant-seed [idea]`**
Capture a forward-looking idea with trigger conditions for automatic surfacing.
- Seeds preserve WHY, WHEN to surface, and breadcrumbs to related code
- Auto-surfaces during `/gsd:new-milestone` when trigger conditions match
- Better than deferred items — triggers are checked, not forgotten
Usage: `/gsd:plant-seed "add real-time notifications when we build the events system"`
---
**`/gsd:audit-uat`**
Cross-phase audit of all outstanding UAT and verification items.
- Scans every phase for pending, skipped, blocked, and human_needed items
- Cross-references against codebase to detect stale documentation
- Produces prioritized human test plan grouped by testability
- Use before starting a new milestone to clear verification debt
Usage: `/gsd:audit-uat`
### Milestone Auditing
**`/gsd:audit-milestone [version]`**

View File

@@ -82,12 +82,25 @@ mkdir -p .planning/codebase
Continue to spawn_agents.
</step>
<step name="spawn_agents">
<step name="detect_runtime_capabilities">
Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation.
**Runtimes with Task tool:** Claude Code, Cursor (native subagent support)
**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others
**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis):
→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead.
**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context.
</step>
<step name="spawn_agents" condition="Task tool is available">
Spawn 4 parallel gsd-codebase-mapper agents.
Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly.
**Agent 1: Tech Focus**
@@ -172,9 +185,19 @@ Continue to collect_confirmations.
</step>
<step name="collect_confirmations">
Wait for all 4 agents to complete.
Wait for all 4 agents to complete using TaskOutput tool.
Read each agent's output file to collect confirmations.
**For each agent task_id returned by the Agent tool calls above:**
```
TaskOutput tool:
task_id: "{task_id from Agent result}"
block: true
timeout: 300000
```
Call TaskOutput for all 4 agents in parallel (single message with 4 TaskOutput calls).
Once all TaskOutput calls return, read each agent's output file to collect confirmations.
**Expected confirmation format from each agent:**
```
@@ -195,6 +218,37 @@ If any agent failed, note the failure and continue with successful documents.
Continue to verify_output.
</step>
<step name="sequential_mapping" condition="Task tool is NOT available (e.g. Antigravity, Gemini CLI, Codex)">
When the `Task` tool is unavailable, perform codebase mapping sequentially in the current context. This replaces `spawn_agents` and `collect_confirmations`.
**IMPORTANT:** Do NOT use `browser_subagent`, `Explore`, or any browser-based tool. Use only file system tools (Read, Bash, Write, Grep, Glob, list_dir, view_file, grep_search, or equivalent tools available in your runtime).
Perform all 4 mapping passes sequentially:
**Pass 1: Tech Focus**
- Explore package.json/Cargo.toml/go.mod/requirements.txt, config files, dependency trees
- Write `.planning/codebase/STACK.md` — Languages, runtime, frameworks, dependencies, configuration
- Write `.planning/codebase/INTEGRATIONS.md` — External APIs, databases, auth providers, webhooks
**Pass 2: Architecture Focus**
- Explore directory structure, entry points, module boundaries, data flow
- Write `.planning/codebase/ARCHITECTURE.md` — Pattern, layers, data flow, abstractions, entry points
- Write `.planning/codebase/STRUCTURE.md` — Directory layout, key locations, naming conventions
**Pass 3: Quality Focus**
- Explore code style, error handling patterns, test files, CI config
- Write `.planning/codebase/CONVENTIONS.md` — Code style, naming, patterns, error handling
- Write `.planning/codebase/TESTING.md` — Framework, structure, mocking, coverage
**Pass 4: Concerns Focus**
- Explore TODOs, known issues, fragile areas, security patterns
- Write `.planning/codebase/CONCERNS.md` — Tech debt, bugs, security, performance, fragile areas
Use the same document templates as the `gsd-codebase-mapper` agent. Include actual file paths formatted with backticks.
Continue to verify_output.
</step>
<step name="verify_output">
Verify all documents created successfully:
@@ -307,10 +361,10 @@ End workflow.
<success_criteria>
- .planning/codebase/ directory created
- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- Agents write documents directly (orchestrator doesn't receive document contents)
- Read agent output files to collect confirmations
- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent)
- All 7 codebase documents exist
- No empty documents (each should have >20 lines)
- Clear completion summary with line counts
- User offered clear next steps in GSD style
</success_criteria>

View File

@@ -14,6 +14,12 @@ Read all files referenced by the invoking prompt's execution_context before star
## 1. Load Context
Parse `$ARGUMENTS` before doing anything else:
- `--reset-phase-numbers` flag → opt into restarting roadmap phase numbering at `1`
- remaining text → use as milestone name if present
If the flag is absent, keep the current behavior of continuing phase numbering from the previous milestone.
- Read PROJECT.md (existing project, validated requirements, decisions)
- Read MILESTONES.md (what shipped previously)
- Read STATE.md (pending todos, blockers)
@@ -54,6 +60,27 @@ Add/update:
Update Active requirements section and "Last updated" footer.
Ensure the `## Evolution` section exists in PROJECT.md. If missing (projects created before this feature), add it before the footer:
```markdown
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd:transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd:complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
```
## 5. Update STATE.md
```markdown
@@ -82,7 +109,27 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`.
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`.
## 7.5 Reset-phase safety (only when `--reset-phase-numbers`)
If `--reset-phase-numbers` is active:
1. Set starting phase number to `1` for the upcoming roadmap.
2. If `phase_dir_count > 0`, archive the old phase directories before roadmapping so new `01-*` / `02-*` directories cannot collide with stale milestone directories.
If `phase_dir_count > 0` and `phase_archive_path` is available:
```bash
mkdir -p "${phase_archive_path}"
find .planning/phases -mindepth 1 -maxdepth 1 -type d -exec mv {} "${phase_archive_path}/" \;
```
Then verify `.planning/phases/` no longer contains old milestone directories before continuing.
If `phase_dir_count > 0` but `phase_archive_path` is missing:
- Stop and explain that reset numbering is unsafe without a completed milestone archive target.
- Tell the user to complete/archive the previous milestone first, then rerun `/gsd:new-milestone --reset-phase-numbers`.
## 8. Research Decision
@@ -270,7 +317,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milest
◆ Spawning roadmapper...
```
**Starting phase number:** Read MILESTONES.md for last phase number. Continue from there (v1.0 ended at phase 5 → v1.1 starts at phase 6).
**Starting phase number:**
- If `--reset-phase-numbers` is active, start at **Phase 1**
- Otherwise, continue from the previous milestone's last phase number (v1.0 ended at phase 5 → v1.1 starts at phase 6)
```
Task(prompt="
@@ -286,7 +335,9 @@ Task(prompt="
<instructions>
Create roadmap for milestone v[X.Y]:
1. Start phase numbering from [N]
1. Respect the selected numbering mode:
- `--reset-phase-numbers` → start at Phase 1
- default behavior → continue from the previous milestone's last phase number
2. Derive phases from THIS MILESTONE's requirements only
3. Map every requirement to exactly one phase
4. Derive 2-5 success criteria per phase (observable user behaviors)
@@ -378,7 +429,7 @@ Also: `/gsd:plan-phase [N]` — skip discussion, plan directly
- [ ] gsd-roadmapper spawned with phase numbering context
- [ ] Roadmap files written immediately (not draft)
- [ ] User feedback incorporated (if any)
- [ ] ROADMAP.md phases continue from previous milestone
- [ ] Phase numbering mode respected (continued or reset)
- [ ] All commits made (if planning docs committed)
- [ ] User knows next step: `/gsd:discuss-phase [N]`

View File

@@ -7,11 +7,13 @@ Read all files referenced by the invoking prompt's execution_context before star
</required_reading>
<auto_mode>
## Auto Mode Detection
Check if `--auto` flag is present in $ARGUMENTS.
**If auto mode:**
- Skip brownfield mapping offer (assume greenfield)
- Skip deep questioning (extract context from provided document)
- Config: YOLO mode is implicit (skip that question), but ask granularity/git/agents FIRST (Step 2a)
@@ -23,6 +25,7 @@ Check if `--auto` flag is present in $ARGUMENTS.
**Document requirement:**
Auto mode requires an idea document — either:
- File reference: `/gsd:new-project --auto @prd.md`
- Pasted/written text in the prompt
@@ -37,6 +40,7 @@ Usage:
The document should describe what you want to build.
```
</auto_mode>
<process>
@@ -55,6 +59,7 @@ Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `co
**If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`.
**If `has_git` is false:** Initialize git:
```bash
git init
```
@@ -66,6 +71,7 @@ git init
**If `needs_codebase_map` is true** (from init — existing code detected but no codebase map):
Use AskUserQuestion:
- header: "Codebase"
- question: "I detected existing code in this directory. Would you like to map the codebase first?"
- options:
@@ -73,9 +79,11 @@ Use AskUserQuestion:
- "Skip mapping" — Proceed with project initialization
**If "Map codebase first":**
```
Run `/gsd:map-codebase` first, then return to `/gsd:new-project`
```
Exit command.
**If "Skip mapping" OR `needs_codebase_map` is false:** Continue to Step 3.
@@ -210,11 +218,20 @@ Ask inline (freeform, NOT AskUserQuestion):
Wait for their response. This gives you the context needed to ask intelligent follow-up questions.
**Research-before-questions mode:** Check if `research_questions` is enabled in `.planning/config.json` (or the config from init context). When enabled, before asking follow-up questions about a topic area:
1. Do a brief web search for best practices related to what the user described
2. Mention key findings naturally as you ask questions (e.g., "Most projects like this use X — is that what you're thinking, or something different?")
3. This makes questions more informed without changing the conversational flow
When disabled (default), ask questions directly as before.
**Follow the thread:**
Based on what they said, ask follow-up questions that dig into their response. Use AskUserQuestion with options that probe what they mentioned — interpretations, clarifications, concrete examples.
Keep following threads. Each answer opens new threads to explore. Ask about:
- What excited them
- What problem sparked this
- What they mean by vague terms
@@ -222,6 +239,7 @@ Keep following threads. Each answer opens new threads to explore. Ask about:
- What's already decided
Consult `questioning.md` for techniques:
- Challenge vagueness
- Make abstract concrete
- Surface assumptions
@@ -323,6 +341,27 @@ Initialize with any decisions made during questioning:
*Last updated: [date] after initialization*
```
**Evolution section** (include at the end of PROJECT.md, before the footer):
```markdown
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd:transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd:complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
```
Do not compress. Capture everything gathered.
**Commit PROJECT.md:**
@@ -465,10 +504,12 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode"
**Note:** Run `/gsd:settings` anytime to update model profile, workflow agents, branching strategy, and other preferences.
**If commit_docs = No:**
- Set `commit_docs: false` in config.json
- Add `.planning/` to `.gitignore` (create if needed)
**If commit_docs = Yes:**
- No additional gitignore entries needed
**Commit config.json:**
@@ -477,6 +518,38 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode"
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project config" --files .planning/config.json
```
## 5.1. Sub-Repo Detection
**Detect multi-repo workspace:**
Check for directories with their own `.git` folders (separate repos within the workspace):
```bash
find . -maxdepth 1 -type d -not -name ".*" -not -name "node_modules" -exec test -d "{}/.git" \; -print
```
**If sub-repos found:**
Strip the `./` prefix to get directory names (e.g., `./backend` → `backend`).
Use AskUserQuestion:
- header: "Multi-Repo Workspace"
- question: "I detected separate git repos in this workspace. Which directories contain code that GSD should commit to?"
- multiSelect: true
- options: one option per detected directory
- "[directory name]" — Separate git repo
**If user selects one or more directories:**
- Set `planning.sub_repos` in config.json to the selected directory names array (e.g., `["backend", "frontend"]`)
- Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces)
- Add `.planning/` to `.gitignore` if not already present
Config changes are saved locally — no commit needed since `commit_docs` is `false` in multi-repo mode.
**If no sub-repos found or user selects none:** Continue with no changes to config.
## 5.5. Resolve Model Profile
Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model`.
@@ -486,6 +559,7 @@ Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model
**If auto mode:** Default to "Research first" without asking.
Use AskUserQuestion:
- header: "Research"
- question: "Research the domain ecosystem before defining requirements?"
- options:
@@ -495,6 +569,7 @@ Use AskUserQuestion:
**If "Research first":**
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► RESEARCHING
@@ -504,6 +579,7 @@ Researching [domain] ecosystem...
```
Create research directory:
```bash
mkdir -p .planning/research
```
@@ -511,10 +587,12 @@ mkdir -p .planning/research
**Determine milestone context:**
Check if this is greenfield or subsequent milestone:
- If no "Validated" requirements in PROJECT.md → Greenfield (building from scratch)
- If "Validated" requirements exist → Subsequent milestone (adding to existing app)
Display spawning indicator:
```
◆ Spawning 4 researchers in parallel...
→ Stack research
@@ -703,6 +781,7 @@ Commit after writing.
```
Display research complete banner and key findings:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► RESEARCH COMPLETE ✓
@@ -722,6 +801,7 @@ Files: `.planning/research/`
## 7. Define Requirements
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► DEFINING REQUIREMENTS
@@ -731,6 +811,7 @@ Display stage banner:
**Load context:**
Read PROJECT.md and extract:
- Core value (the ONE thing that must work)
- Stated constraints (budget, timeline, tech limitations)
- Any explicit scope boundaries
@@ -738,6 +819,7 @@ Read PROJECT.md and extract:
**If research exists:** Read research/FEATURES.md and extract feature categories.
**If auto mode:**
- Auto-include all table stakes features (users expect these)
- Include features explicitly mentioned in provided document
- Auto-defer differentiators not mentioned in document
@@ -776,6 +858,7 @@ Here are the features for [domain]:
Ask: "What are the main things users need to be able to do?"
For each capability mentioned:
- Ask clarifying questions to make it specific
- Probe for related capabilities
- Group into categories
@@ -794,6 +877,7 @@ For each category, use AskUserQuestion:
- "None for v1" — Defer entire category
Track responses:
- Selected features → v1 requirements
- Unselected table stakes → v2 (users expect these)
- Unselected differentiators → out of scope
@@ -801,6 +885,7 @@ Track responses:
**Identify gaps:**
Use AskUserQuestion:
- header: "Additions"
- question: "Any requirements research missed? (Features specific to your vision)"
- options:
@@ -814,6 +899,7 @@ Cross-check requirements against Core Value from PROJECT.md. If gaps detected, s
**Generate REQUIREMENTS.md:**
Create `.planning/REQUIREMENTS.md` with:
- v1 Requirements grouped by category (checkboxes, REQ-IDs)
- v2 Requirements (deferred)
- Out of Scope (explicit exclusions with reasoning)
@@ -824,12 +910,14 @@ Create `.planning/REQUIREMENTS.md` with:
**Requirement quality criteria:**
Good requirements are:
- **Specific and testable:** "User can reset password via email link" (not "Handle password reset")
- **User-centric:** "User can X" (not "System does Y")
- **Atomic:** One capability per requirement (not "User can login and manage profile")
- **Independent:** Minimal dependencies on other requirements
Reject vague requirements. Push for specificity:
- "Handle authentication" → "User can log in with email/password and stay logged in across sessions"
- "Support sharing" → "User can share post via link that opens in recipient's browser"
@@ -867,6 +955,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define v1 req
## 8. Create Roadmap
Display stage banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► CREATING ROADMAP
@@ -907,6 +996,7 @@ Write files first, then return. This ensures artifacts persist even if context i
**Handle roadmapper return:**
**If `## ROADMAP BLOCKED`:**
- Present blocker information
- Work with user to resolve
- Re-spawn when resolved
@@ -956,6 +1046,7 @@ Success criteria:
**CRITICAL: Ask for approval before committing (interactive mode only):**
Use AskUserQuestion:
- header: "Roadmap"
- question: "Does this roadmap structure work for you?"
- options:
@@ -966,8 +1057,10 @@ Use AskUserQuestion:
**If "Approve":** Continue to commit.
**If "Adjust phases":**
- Get user's adjustment notes
- Re-spawn roadmapper with revision context:
```
Task(prompt="
<revision>
@@ -983,15 +1076,24 @@ Use AskUserQuestion:
</revision>
", subagent_type="gsd-roadmapper", model="{roadmapper_model}", description="Revise roadmap")
```
- Present revised roadmap
- Loop until user approves
**If "Review full file":** Display raw `cat .planning/ROADMAP.md`, then re-ask.
**Generate or refresh project CLAUDE.md before final commit:**
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-claude-md
```
This ensures new projects get the default GSD workflow-enforcement guidance and current project context in `CLAUDE.md`.
**Commit roadmap (after approval or auto mode):**
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md
```
## 9. Done
@@ -1012,6 +1114,7 @@ Present completion summary:
| Research | `.planning/research/` |
| Requirements | `.planning/REQUIREMENTS.md` |
| Roadmap | `.planning/ROADMAP.md` |
| Project guide | `CLAUDE.md` |
**[N] phases** | **[X] requirements** | Ready to build ✓
```
@@ -1062,6 +1165,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto")
- `.planning/REQUIREMENTS.md`
- `.planning/ROADMAP.md`
- `.planning/STATE.md`
- `CLAUDE.md`
</output>
@@ -1083,6 +1187,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto")
- [ ] ROADMAP.md created with phases, requirement mappings, success criteria
- [ ] STATE.md initialized
- [ ] REQUIREMENTS.md traceability updated
- [ ] CLAUDE.md generated with GSD workflow guidance
- [ ] User knows next step is `/gsd:discuss-phase 1`
**Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist.

View File

@@ -0,0 +1,97 @@
<purpose>
Detect current project state and automatically advance to the next logical GSD workflow step.
Reads project state to determine: discuss → plan → execute → verify → complete progression.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="detect_state">
Read project state to determine current position:
```bash
# Get state snapshot
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state json 2>/dev/null || echo "{}"
```
Also read:
- `.planning/STATE.md` — current phase, progress, plan counts
- `.planning/ROADMAP.md` — milestone structure and phase list
Extract:
- `current_phase` — which phase is active
- `plan_of` / `plans_total` — plan execution progress
- `progress` — overall percentage
- `status` — active, paused, etc.
If no `.planning/` directory exists:
```
No GSD project detected. Run `/gsd:new-project` to get started.
```
Exit.
</step>
<step name="determine_next_action">
Apply routing rules based on state:
**Route 1: No phases exist yet → discuss**
If ROADMAP has phases but no phase directories exist on disk:
→ Next action: `/gsd:discuss-phase <first-phase>`
**Route 2: Phase exists but has no CONTEXT.md or RESEARCH.md → discuss**
If the current phase directory exists but has neither CONTEXT.md nor RESEARCH.md:
→ Next action: `/gsd:discuss-phase <current-phase>`
**Route 3: Phase has context but no plans → plan**
If the current phase has CONTEXT.md (or RESEARCH.md) but no PLAN.md files:
→ Next action: `/gsd:plan-phase <current-phase>`
**Route 4: Phase has plans but incomplete summaries → execute**
If plans exist but not all have matching summaries:
→ Next action: `/gsd:execute-phase <current-phase>`
**Route 5: All plans have summaries → verify and complete**
If all plans in the current phase have summaries:
→ Next action: `/gsd:verify-work` then `/gsd:complete-phase`
**Route 6: Phase complete, next phase exists → advance**
If the current phase is complete and the next phase exists in ROADMAP:
→ Next action: `/gsd:discuss-phase <next-phase>`
**Route 7: All phases complete → complete milestone**
If all phases are complete:
→ Next action: `/gsd:complete-milestone`
**Route 8: Paused → resume**
If STATE.md shows paused_at:
→ Next action: `/gsd:resume-work`
</step>
<step name="show_and_execute">
Display the determination:
```
## GSD Next
**Current:** Phase [N] — [name] | [progress]%
**Status:** [status description]
▶ **Next step:** `/gsd:[command] [args]`
[One-line explanation of why this is the next step]
```
Then immediately invoke the determined command via SlashCommand.
Do not ask for confirmation — the whole point of `/gsd:next` is zero-friction advancement.
</step>
</process>
<success_criteria>
- [ ] Project state correctly detected
- [ ] Next action correctly determined from routing rules
- [ ] Command invoked immediately without user confirmation
- [ ] Clear status shown before invoking
</success_criteria>

View File

@@ -1,5 +1,5 @@
<purpose>
Create `.continue-here.md` handoff file to preserve complete work state across sessions. Enables seamless resumption with full context restoration.
Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context.
</purpose>
<required_reading>
@@ -27,10 +27,61 @@ If no active phase detected, ask user which phase they're pausing work on.
3. **Work remaining**: What's left in current plan/phase
4. **Decisions made**: Key decisions and rationale
5. **Blockers/issues**: Anything stuck
6. **Mental context**: The approach, next steps, "vibe"
7. **Files modified**: What's changed but not committed
6. **Human actions pending**: Things that need manual intervention (MCP setup, API keys, approvals, manual testing)
7. **Background processes**: Any running servers/watchers that were part of the workflow
8. **Files modified**: What's changed but not committed
Ask user for clarifications if needed via conversational questions.
**Also inspect SUMMARY.md files for false completions:**
```bash
# Check for placeholder content in existing summaries
grep -l "To be filled\|placeholder\|TBD" .planning/phases/*/*.md 2>/dev/null
```
Report any summaries with placeholder content as incomplete items.
</step>
<step name="write_structured">
**Write structured handoff to `.planning/HANDOFF.json`:**
```bash
timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timestamp full --raw)
```
```json
{
"version": "1.0",
"timestamp": "{timestamp}",
"phase": "{phase_number}",
"phase_name": "{phase_name}",
"phase_dir": "{phase_dir}",
"plan": {current_plan_number},
"task": {current_task_number},
"total_tasks": {total_task_count},
"status": "paused",
"completed_tasks": [
{"id": 1, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
{"id": 2, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
{"id": 3, "name": "{task_name}", "status": "in_progress", "progress": "{what_done}"}
],
"remaining_tasks": [
{"id": 4, "name": "{task_name}", "status": "not_started"},
{"id": 5, "name": "{task_name}", "status": "not_started"}
],
"blockers": [
{"description": "{blocker}", "type": "technical|human_action|external", "workaround": "{if any}"}
],
"human_actions_pending": [
{"action": "{what needs to be done}", "context": "{why}", "blocking": true}
],
"decisions": [
{"decision": "{what}", "rationale": "{why}", "phase": "{phase_number}"}
],
"uncommitted_files": [],
"next_action": "{specific first action when resuming}",
"context_notes": "{mental state, approach, what you were thinking}"
}
```
</step>
<step name="write">
@@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta
<step name="commit">
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md .planning/HANDOFF.json
```
</step>
<step name="confirm">
```
✓ Handoff created: .planning/phases/[XX-name]/.continue-here.md
✓ Handoff created:
- .planning/HANDOFF.json (structured, machine-readable)
- .planning/phases/[XX-name]/.continue-here.md (human-readable)
Current state:
- Phase: [XX-name]
- Task: [X] of [Y]
- Status: [in_progress/blocked]
- Blockers: [count] ({human_actions_pending count} need human action)
- Committed as WIP
To resume: /gsd:resume-work

View File

@@ -8,6 +8,13 @@ Read all files referenced by the invoking prompt's execution_context before star
@~/.claude/get-shit-done/references/ui-brand.md
</required_reading>
<available_agent_types>
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-planner — Creates detailed plans from phase scope
- gsd-plan-checker — Reviews plan quality before execution
</available_agent_types>
<process>
## 1. Initialize
@@ -170,7 +177,16 @@ Use AskUserQuestion:
- "Run discuss-phase first" — Capture design decisions before planning
If "Continue without context": Proceed to step 5.
If "Run discuss-phase first": Display `/gsd:discuss-phase {X}` and exit workflow.
If "Run discuss-phase first":
**IMPORTANT:** Do NOT invoke discuss-phase as a nested Skill/Task call — AskUserQuestion
does not work correctly in nested subcontexts (#1009). Instead, display the command
and exit so the user runs it as a top-level command:
```
Run this command first, then re-run /gsd:plan-phase {X}:
/gsd:discuss-phase {X}
```
**Exit the plan-phase workflow. Do not continue.**
## 5. Handle Research
@@ -571,11 +587,62 @@ Display: `Max iterations reached. {N} issues remain:` + issue list
Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
## 13. Present Final Status
## 13. Requirements Coverage Gate
After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.
**Skip if:** `phase_req_ids` is null or TBD (no requirements mapped to this phase).
**Step 1: Extract requirement IDs claimed by plans**
```bash
# Collect all requirement IDs from plan frontmatter
PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u)
```
**Step 2: Compare against phase requirements from ROADMAP**
For each REQ-ID in `phase_req_ids`:
- If REQ-ID appears in `PLAN_REQS` → covered ✓
- If REQ-ID does NOT appear in any plan → uncovered ✗
**Step 3: Check CONTEXT.md features against plan objectives**
Read CONTEXT.md `<decisions>` section. Extract feature/capability names. Check each against plan `<objective>` blocks. Features not mentioned in any plan objective → potentially dropped.
**Step 4: Report**
If all requirements covered and no dropped features:
```
✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans
```
→ Proceed to step 14.
If gaps found:
```
## ⚠ Requirements Coverage Gap
{M} of {N} phase requirements are not assigned to any plan:
| REQ-ID | Description | Plans |
|--------|-------------|-------|
| {id} | {from REQUIREMENTS.md} | None |
{K} CONTEXT.md features not found in plan objectives:
- {feature_name} — described in CONTEXT.md but no plan covers it
Options:
1. Re-plan to include missing requirements (recommended)
2. Move uncovered requirements to next phase
3. Proceed anyway — accept coverage gaps
```
Use AskUserQuestion to present the options.
## 14. Present Final Status
Route to `<offer_next>` OR `auto_advance` depending on flags/config.
## 14. Auto-Advance Check
## 15. Auto-Advance Check
Check for auto-advance trigger:
@@ -670,6 +737,30 @@ Verification: {Passed | Passed with override | Skipped}
───────────────────────────────────────────────────────────────
</offer_next>
<windows_troubleshooting>
**Windows users:** If plan-phase freezes during agent spawning (common on Windows due to
stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126):
1. **Force-kill:** Close the terminal (Ctrl+C may not work)
2. **Clean up orphaned processes:**
```powershell
# Kill orphaned node processes from stale MCP servers
Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force
```
3. **Clean up stale task directories:**
```powershell
# Remove stale subagent task dirs (Claude Code never cleans these on crash)
Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue
```
4. **Reduce MCP server count:** Temporarily disable non-essential MCP servers in settings.json
5. **Retry:** Restart Claude Code and run `/gsd:plan-phase` again
If freezes persist, try `--skip-research` to reduce the agent chain from 3 to 2 agents:
```
/gsd:plan-phase N --skip-research
```
</windows_troubleshooting>
<success_criteria>
- [ ] .planning/ directory validated
- [ ] Phase validated against roadmap

View File

@@ -0,0 +1,169 @@
<purpose>
Capture a forward-looking idea as a structured seed file with trigger conditions.
Seeds auto-surface during /gsd:new-milestone when trigger conditions match the
new milestone's scope.
Seeds beat deferred items because they:
- Preserve WHY the idea matters (not just WHAT)
- Define WHEN to surface (trigger conditions, not manual scanning)
- Track breadcrumbs (code references, related decisions)
- Auto-present at the right time via new-milestone scan
</purpose>
<process>
<step name="parse_idea">
Parse `$ARGUMENTS` for the idea summary.
If empty, ask:
```
What's the idea? (one sentence)
```
Store as `$IDEA`.
</step>
<step name="create_seed_dir">
```bash
mkdir -p .planning/seeds
```
</step>
<step name="gather_context">
Ask focused questions to build a complete seed:
```
AskUserQuestion(
header: "Trigger",
question: "When should this idea surface? (e.g., 'when we add user accounts', 'next major version', 'when performance becomes a priority')",
options: [] // freeform
)
```
Store as `$TRIGGER`.
```
AskUserQuestion(
header: "Why",
question: "Why does this matter? What problem does it solve or what opportunity does it create?",
options: []
)
```
Store as `$WHY`.
```
AskUserQuestion(
header: "Scope",
question: "How big is this? (rough estimate)",
options: [
{ label: "Small", description: "A few hours — could be a quick task" },
{ label: "Medium", description: "A phase or two — needs planning" },
{ label: "Large", description: "A full milestone — significant effort" }
]
)
```
Store as `$SCOPE`.
</step>
<step name="collect_breadcrumbs">
Search the codebase for relevant references:
```bash
# Find files related to the idea keywords
grep -rl "$KEYWORD" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null | head -10
```
Also check:
- Current STATE.md for related decisions
- ROADMAP.md for related phases
- todos/ for related captured ideas
Store relevant file paths as `$BREADCRUMBS`.
</step>
<step name="generate_seed_id">
```bash
# Find next seed number
EXISTING=$(ls .planning/seeds/SEED-*.md 2>/dev/null | wc -l)
NEXT=$((EXISTING + 1))
PADDED=$(printf "%03d" $NEXT)
```
Generate slug from idea summary.
</step>
<step name="write_seed">
Write `.planning/seeds/SEED-{PADDED}-{slug}.md`:
```markdown
---
id: SEED-{PADDED}
status: dormant
planted: {ISO date}
planted_during: {current milestone/phase from STATE.md}
trigger_when: {$TRIGGER}
scope: {$SCOPE}
---
# SEED-{PADDED}: {$IDEA}
## Why This Matters
{$WHY}
## When to Surface
**Trigger:** {$TRIGGER}
This seed should be presented during `/gsd:new-milestone` when the milestone
scope matches any of these conditions:
- {trigger condition 1}
- {trigger condition 2}
## Scope Estimate
**{$SCOPE}** — {elaboration based on scope choice}
## Breadcrumbs
Related code and decisions found in the current codebase:
{list of $BREADCRUMBS with file paths}
## Notes
{any additional context from the current session}
```
</step>
<step name="commit_seed">
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md
```
</step>
<step name="confirm">
```
✅ Seed planted: SEED-{PADDED}
"{$IDEA}"
Trigger: {$TRIGGER}
Scope: {$SCOPE}
File: .planning/seeds/SEED-{PADDED}-{slug}.md
This seed will surface automatically when you run /gsd:new-milestone
and the milestone scope matches the trigger condition.
```
</step>
</process>
<success_criteria>
- [ ] Seed file created in .planning/seeds/
- [ ] Frontmatter includes status, trigger, scope
- [ ] Breadcrumbs collected from codebase
- [ ] Committed to git
- [ ] User shown confirmation with trigger info
</success_criteria>

View File

@@ -0,0 +1,129 @@
<purpose>
Create a clean branch for pull requests by filtering out .planning/ commits.
The PR branch contains only code changes — reviewers don't see GSD artifacts
(PLAN.md, SUMMARY.md, STATE.md, CONTEXT.md, etc.).
Uses git cherry-pick with path filtering to rebuild a clean history.
</purpose>
<process>
<step name="detect_state">
Parse `$ARGUMENTS` for target branch (default: `main`).
```bash
CURRENT_BRANCH=$(git branch --show-current)
TARGET=${1:-main}
```
Check preconditions:
- Must be on a feature branch (not main/master)
- Must have commits ahead of target
```bash
AHEAD=$(git rev-list --count "$TARGET".."$CURRENT_BRANCH" 2>/dev/null)
if [ "$AHEAD" = "0" ]; then
echo "No commits ahead of $TARGET — nothing to filter."
exit 0
fi
```
Display:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► PR BRANCH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Branch: {CURRENT_BRANCH}
Target: {TARGET}
Commits: {AHEAD} ahead
```
</step>
<step name="analyze_commits">
Classify commits:
```bash
# Get all commits ahead of target
git log --oneline "$TARGET".."$CURRENT_BRANCH" --no-merges
```
For each commit, check if it ONLY touches .planning/ files:
```bash
# For each commit hash
FILES=$(git diff-tree --no-commit-id --name-only -r $HASH)
ALL_PLANNING=$(echo "$FILES" | grep -v "^\.planning/" | wc -l)
```
Classify:
- **Code commits**: Touch at least one non-.planning/ file → INCLUDE
- **Planning-only commits**: Touch only .planning/ files → EXCLUDE
- **Mixed commits**: Touch both → INCLUDE (planning changes come along)
Display analysis:
```
Commits to include: {N} (code changes)
Commits to exclude: {N} (planning-only)
Mixed commits: {N} (code + planning — included)
```
</step>
<step name="create_pr_branch">
```bash
PR_BRANCH="${CURRENT_BRANCH}-pr"
# Create PR branch from target
git checkout -b "$PR_BRANCH" "$TARGET"
```
Cherry-pick only code commits (in order):
```bash
for HASH in $CODE_COMMITS; do
git cherry-pick "$HASH" --no-commit
# Remove any .planning/ files that came along in mixed commits
git rm -r --cached .planning/ 2>/dev/null || true
git commit -C "$HASH"
done
```
Return to original branch:
```bash
git checkout "$CURRENT_BRANCH"
```
</step>
<step name="verify">
```bash
# Verify no .planning/ files in PR branch
PLANNING_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | grep "^\.planning/" | wc -l)
TOTAL_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | wc -l)
PR_COMMITS=$(git rev-list --count "$TARGET".."$PR_BRANCH")
```
Display results:
```
✅ PR branch created: {PR_BRANCH}
Original: {AHEAD} commits, {ORIGINAL_FILES} files
PR branch: {PR_COMMITS} commits, {TOTAL_FILES} files
Planning files: {PLANNING_FILES} (should be 0)
Next steps:
git push origin {PR_BRANCH}
gh pr create --base {TARGET} --head {PR_BRANCH}
Or use /gsd:ship to create the PR automatically.
```
</step>
</process>
<success_criteria>
- [ ] PR branch created from target
- [ ] Planning-only commits excluded
- [ ] No .planning/ files in PR branch diff
- [ ] Commit messages preserved from original
- [ ] User shown next steps
</success_criteria>

View File

@@ -150,17 +150,47 @@ State: "This phase has {X} plans, {Y} summaries."
Check for UAT.md files with status "diagnosed" (has gaps needing fixes).
```bash
# Check for diagnosed UAT with gaps
grep -l "status: diagnosed" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null
# Check for diagnosed UAT with gaps or partial (incomplete) testing
grep -l "status: diagnosed\|status: partial" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null
```
Track:
- `uat_with_gaps`: UAT.md files with status "diagnosed" (gaps need fixing)
- `uat_partial`: UAT.md files with status "partial" (incomplete testing)
**Step 1.6: Cross-phase health check**
Scan ALL phases in the current milestone for outstanding verification debt using the CLI (which respects milestone boundaries via `getMilestonePhaseFilter`):
```bash
DEBT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw 2>/dev/null)
```
Parse JSON for `summary.total_items` and `summary.total_files`.
Track: `outstanding_debt` — `summary.total_items` from the audit.
**If outstanding_debt > 0:** Add a warning section to the progress report output (in the `report` step), placed between "## What's Next" and the route suggestion:
```markdown
## Verification Debt ({N} files across prior phases)
| Phase | File | Issue |
|-------|------|-------|
| {phase} | {filename} | {pending_count} pending, {skipped_count} skipped, {blocked_count} blocked |
| {phase} | {filename} | human_needed — {count} items |
Review: `/gsd:audit-uat` — full cross-phase audit
Resume testing: `/gsd:verify-work {phase}` — retest specific phase
```
This is a WARNING, not a blocker — routing proceeds normally. The debt is visible so the user can make an informed choice.
**Step 2: Route based on counts**
| Condition | Meaning | Action |
|-----------|---------|--------|
| uat_partial > 0 | UAT testing incomplete | Go to **Route E.2** |
| uat_with_gaps > 0 | UAT gaps need fix plans | Go to **Route E** |
| summaries < plans | Unexecuted plans exist | Go to **Route A** |
| summaries = plans AND plans > 0 | Phase complete | Go to Step 3 |
@@ -260,6 +290,32 @@ UAT.md exists with gaps (diagnosed issues). User needs to plan fixes.
---
**Route E.2: UAT testing incomplete (partial)**
UAT.md exists with `status: partial` — testing session ended before all items resolved.
```
---
## Incomplete UAT Testing
**{phase_num}-UAT.md** has {N} unresolved tests (pending, blocked, or skipped).
`/gsd:verify-work {phase}` — resume testing from where you left off
<sub>`/clear` first → fresh context window</sub>
---
**Also available:**
- `/gsd:audit-uat` — full cross-phase UAT audit
- `/gsd:execute-phase {phase}` — execute phase plans
---
```
---
**Step 3: Check milestone status (only when phase complete)**
Read ROADMAP.md and identify:

View File

@@ -111,7 +111,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init quick "$DESCRIP
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
**If `roadmap_exists` is false:** Error — Quick mode requires an active project with ROADMAP.md. Run `/gsd:new-project` first.
@@ -119,6 +119,20 @@ Quick tasks can run mid-phase - validation only checks ROADMAP.md exists, not ph
---
**Step 2.5: Handle quick-task branching**
**If `branch_name` is empty/null:** Skip and continue on the current branch.
**If `branch_name` is set:** Check out the quick-task branch before any planning commits:
```bash
git checkout -b "$branch_name" 2>/dev/null || git checkout "$branch_name"
```
All quick-task commits for this run stay on that branch. User handles merge/rebase afterward.
---
**Step 3: Create task directory**
```bash

View File

@@ -63,6 +63,9 @@ cat .planning/PROJECT.md
Look for incomplete work that needs attention:
```bash
# Check for structured handoff (preferred — machine-readable)
cat .planning/HANDOFF.json 2>/dev/null
# Check for continue-here files (mid-plan resumption)
ls .planning/phases/*/.continue-here*.md 2>/dev/null
@@ -78,7 +81,18 @@ if [ "$has_interrupted_agent" = "true" ]; then
fi
```
**If .continue-here file exists:**
**If HANDOFF.json exists:**
- This is the primary resumption source — structured data from `/gsd:pause-work`
- Parse `status`, `phase`, `plan`, `task`, `total_tasks`, `next_action`
- Check `blockers` and `human_actions_pending` — surface these immediately
- Check `completed_tasks` for `in_progress` items — these need attention first
- Validate `uncommitted_files` against `git status` — flag divergence
- Use `context_notes` to restore mental model
- Flag: "Found structured handoff — resuming from task {task}/{total_tasks}"
- **After successful resumption, delete HANDOFF.json** (it's a one-shot artifact)
**If .continue-here file exists (fallback):**
- This is a mid-plan resumption point
- Read the file for specific resumption context
@@ -145,8 +159,12 @@ Based on project state, determine the most logical next action:
→ Primary: Resume interrupted agent (Task tool with resume parameter)
→ Option: Start fresh (abandon agent work)
**If HANDOFF.json exists:**
→ Primary: Resume from structured handoff (highest priority — specific task/blocker context)
→ Option: Discard handoff and reassess from files
**If .continue-here file exists:**
→ Primary: Resume from checkpoint
→ Fallback: Resume from checkpoint
→ Option: Start fresh on current plan
**If incomplete plan (PLAN without SUMMARY):**
@@ -154,7 +172,7 @@ Based on project state, determine the most logical next action:
→ Option: Abandon and move on
**If phase in progress, all plans complete:**
→ Primary: Transition to next phase
→ Primary: Advance to next phase (via internal transition workflow)
→ Option: Review completed work
**If phase ready to plan:**
@@ -242,7 +260,7 @@ Based on user selection, route to appropriate workflow:
---
```
- **Transition** → ./transition.md
- **Advance to next phase** → ./transition.md (internal workflow, invoked inline — NOT a user command)
- **Check todos** → Read .planning/todos/pending/, present summary
- **Review alignment** → Read PROJECT.md, compare to current state
- **Something else** → Ask what they need

View File

@@ -0,0 +1,228 @@
<purpose>
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
Each CLI gets the same prompt (PROJECT.md context, phase plans, requirements) and
produces structured feedback. Results are combined into REVIEWS.md for the planner
to incorporate via --reviews flag.
This implements adversarial review: different AI models catch different blind spots.
A plan that survives review from 2-3 independent AI systems is more robust.
</purpose>
<process>
<step name="detect_clis">
Check which AI CLIs are available on the system:
```bash
# Check each CLI
command -v gemini >/dev/null 2>&1 && echo "gemini:available" || echo "gemini:missing"
command -v claude >/dev/null 2>&1 && echo "claude:available" || echo "claude:missing"
command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missing"
```
Parse flags from `$ARGUMENTS`:
- `--gemini` → include Gemini
- `--claude` → include Claude
- `--codex` → include Codex
- `--all` → include all available
- No flags → include all available
If no CLIs are available:
```
No external AI CLIs found. Install at least one:
- gemini: https://github.com/google-gemini/gemini-cli
- codex: https://github.com/openai/codex
- claude: https://github.com/anthropics/claude-code
Then run /gsd:review again.
```
Exit.
If only one CLI is the current runtime (e.g. running inside Claude), skip it for the review
to ensure independence. At least one DIFFERENT CLI must be available.
</step>
<step name="gather_context">
Collect phase artifacts for the review prompt:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Read from init: `phase_dir`, `phase_number`, `padded_phase`.
Then read:
1. `.planning/PROJECT.md` (first 80 lines — project context)
2. Phase section from `.planning/ROADMAP.md`
3. All `*-PLAN.md` files in the phase directory
4. `*-CONTEXT.md` if present (user decisions)
5. `*-RESEARCH.md` if present (domain research)
6. `.planning/REQUIREMENTS.md` (requirements this phase addresses)
</step>
<step name="build_prompt">
Build a structured review prompt:
```markdown
# Cross-AI Plan Review Request
You are reviewing implementation plans for a software project phase.
Provide structured feedback on plan quality, completeness, and risks.
## Project Context
{first 80 lines of PROJECT.md}
## Phase {N}: {phase name}
### Roadmap Section
{roadmap phase section}
### Requirements Addressed
{requirements for this phase}
### User Decisions (CONTEXT.md)
{context if present}
### Research Findings
{research if present}
### Plans to Review
{all PLAN.md contents}
## Review Instructions
Analyze each plan and provide:
1. **Summary** — One-paragraph assessment
2. **Strengths** — What's well-designed (bullet points)
3. **Concerns** — Potential issues, gaps, risks (bullet points with severity: HIGH/MEDIUM/LOW)
4. **Suggestions** — Specific improvements (bullet points)
5. **Risk Assessment** — Overall risk level (LOW/MEDIUM/HIGH) with justification
Focus on:
- Missing edge cases or error handling
- Dependency ordering issues
- Scope creep or over-engineering
- Security considerations
- Performance implications
- Whether the plans actually achieve the phase goals
Output your review in markdown format.
```
Write to a temp file: `/tmp/gsd-review-prompt-{phase}.md`
</step>
<step name="invoke_reviewers">
For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
**Gemini:**
```bash
gemini -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-gemini-{phase}.md
```
**Claude (separate session):**
```bash
claude -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" --no-input 2>/dev/null > /tmp/gsd-review-claude-{phase}.md
```
**Codex:**
```bash
codex -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
```
If a CLI fails, log the error and continue with remaining CLIs.
Display progress:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► CROSS-AI REVIEW — Phase {N}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Reviewing with {CLI}... done ✓
◆ Reviewing with {CLI}... done ✓
```
</step>
<step name="write_reviews">
Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`:
```markdown
---
phase: {N}
reviewers: [gemini, claude, codex]
reviewed_at: {ISO timestamp}
plans_reviewed: [{list of PLAN.md files}]
---
# Cross-AI Plan Review — Phase {N}
## Gemini Review
{gemini review content}
---
## Claude Review
{claude review content}
---
## Codex Review
{codex review content}
---
## Consensus Summary
{synthesize common concerns across all reviewers}
### Agreed Strengths
{strengths mentioned by 2+ reviewers}
### Agreed Concerns
{concerns raised by 2+ reviewers — highest priority}
### Divergent Views
{where reviewers disagreed — worth investigating}
```
Commit:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: cross-AI review for phase {N}" --files {phase_dir}/{padded_phase}-REVIEWS.md
```
</step>
<step name="present_results">
Display summary:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► REVIEW COMPLETE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase {N} reviewed by {count} AI systems.
Consensus concerns:
{top 3 shared concerns}
Full review: {padded_phase}-REVIEWS.md
To incorporate feedback into planning:
/gsd:plan-phase {N} --reviews
```
Clean up temp files.
</step>
</process>
<success_criteria>
- [ ] At least one external CLI invoked successfully
- [ ] REVIEWS.md written with structured feedback
- [ ] Consensus summary synthesized from multiple reviewers
- [ ] Temp files cleaned up
- [ ] User knows how to use feedback (/gsd:plan-phase --reviews)
</success_criteria>

View File

@@ -0,0 +1,146 @@
<purpose>
Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="gather_session_data">
Collect session data from available sources:
1. **STATE.md** — current phase, milestone, progress, blockers, decisions
2. **Git log** — commits made during this session (last 24h or since last report)
3. **Plan/Summary files** — plans executed, summaries written
4. **ROADMAP.md** — milestone context and phase goals
```bash
# Get recent commits (last 24 hours)
git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits"
# Count files changed
git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available"
```
Read `.planning/STATE.md` to get:
- Current milestone and phase
- Progress percentage
- Active blockers
- Recent decisions
Read `.planning/ROADMAP.md` to get milestone name and goals.
Check for existing reports:
```bash
ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports"
```
</step>
<step name="estimate_usage">
Estimate token usage from observable signals:
- Count of tool calls is not directly available, so estimate from git activity and file operations
- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks
Estimation heuristics:
- Each commit ≈ 1 plan cycle (research + plan + execute + verify)
- Each plan file ≈ 2,000-5,000 tokens of agent context
- Each summary file ≈ 1,000-2,000 tokens generated
- Subagent spawns multiply by ~1.5x per agent type used
</step>
<step name="generate_report">
Create the report directory and file:
```bash
mkdir -p .planning/reports
```
Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist):
```markdown
# GSD Session Report
**Generated:** [timestamp]
**Project:** [from PROJECT.md title or directory name]
**Milestone:** [N] — [milestone name from ROADMAP.md]
---
## Session Summary
**Duration:** [estimated from first to last commit timestamp, or "Single session"]
**Phase Progress:** [from STATE.md]
**Plans Executed:** [count of summaries written this session]
**Commits Made:** [count from git log]
## Work Performed
### Phases Touched
[List phases worked on with brief description of what was done]
### Key Outcomes
[Bullet list of concrete deliverables: files created, features implemented, bugs fixed]
### Decisions Made
[From STATE.md decisions table, if any were added this session]
## Files Changed
[Summary of files modified, created, deleted — from git diff stat]
## Blockers & Open Items
[Active blockers from STATE.md]
[Any TODO items created during session]
## Estimated Resource Usage
| Metric | Estimate |
|--------|----------|
| Commits | [N] |
| Files changed | [N] |
| Plans executed | [N] |
| Subagents spawned | [estimated] |
> **Note:** Token and cost estimates require API-level instrumentation.
> These metrics reflect observable session activity only.
---
*Generated by `/gsd:session-report`*
```
</step>
<step name="display_result">
Show the user:
```
## Session Report Generated
📄 `.planning/reports/[filename].md`
### Highlights
- **Commits:** [N]
- **Files changed:** [N]
- **Phase progress:** [X]%
- **Plans executed:** [N]
```
If this is the first report, mention:
```
💡 Run `/gsd:session-report` at the end of each session to build a history of project activity.
```
</step>
</process>
<success_criteria>
- [ ] Session data gathered from STATE.md, git log, and plan files
- [ ] Report written to .planning/reports/
- [ ] Report includes work summary, outcomes, and file changes
- [ ] Filename includes date to prevent overwrites
- [ ] Result summary displayed to user
</success_criteria>

View File

@@ -49,7 +49,7 @@ AskUserQuestion([
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" },
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
{ label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" }
{ label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" }
]
},
{
@@ -135,6 +135,15 @@ AskUserQuestion([
{ label: "Yes (Recommended)", description: "Warn when context usage exceeds 65%. Helps avoid losing work." },
{ label: "No", description: "Disable warnings. Allows Claude to reach auto-compact naturally. Good for long unattended runs." }
]
},
{
question: "Research best practices before asking questions? (web search during new-project and discuss-phase)",
header: "Research Qs",
multiSelect: false,
options: [
{ label: "No (Recommended)", description: "Ask questions directly. Faster, uses fewer tokens." },
{ label: "Yes", description: "Search web for best practices before each question group. More informed questions but uses more tokens." }
]
}
])
```
@@ -157,10 +166,16 @@ Merge new settings into existing config.json:
"ui_safety_gate": true/false
},
"git": {
"branching_strategy": "none" | "phase" | "milestone"
"branching_strategy": "none" | "phase" | "milestone",
"quick_branch_template": <string|null>
},
"hooks": {
"context_warnings": true/false
"context_warnings": true/false,
"workflow_guard": true/false,
"research_questions": true/false
},
"workflow": {
"text_mode": true/false // Use plain-text questions instead of TUI menus (for /rc remote sessions)
}
}
```
@@ -200,6 +215,7 @@ Write `~/.gsd/defaults.json` with:
"commit_docs": <current>,
"parallelization": <current>,
"branching_strategy": <current>,
"quick_branch_template": <current>,
"workflow": {
"research": <current>,
"plan_check": <current>,

View File

@@ -0,0 +1,228 @@
<purpose>
Create a pull request from completed phase/milestone work, generate a rich PR body from planning artifacts, optionally run code review, and prepare for merge. Closes the plan → execute → verify → ship loop.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="initialize">
Parse arguments and load project state:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
Also load config for branching strategy:
```bash
CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
```
Extract: `branching_strategy`, `branch_name`.
</step>
<step name="preflight_checks">
Verify the work is ready to ship:
1. **Verification passed?**
```bash
VERIFICATION=$(cat ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null)
```
Check for `status: passed` or `status: human_needed` (with human approval).
If no VERIFICATION.md or status is `gaps_found`: warn and ask user to confirm.
2. **Clean working tree?**
```bash
git status --short
```
If uncommitted changes exist: ask user to commit or stash first.
3. **On correct branch?**
```bash
CURRENT_BRANCH=$(git branch --show-current)
```
If on `main`/`master`: warn — should be on a feature branch.
If branching_strategy is `none`: offer to create a branch now.
4. **Remote configured?**
```bash
git remote -v | head -2
```
Detect `origin` remote. If no remote: error — can't create PR.
5. **`gh` CLI available?**
```bash
which gh && gh auth status 2>&1
```
If `gh` not found or not authenticated: provide setup instructions and exit.
</step>
<step name="push_branch">
Push the current branch to remote:
```bash
git push origin ${CURRENT_BRANCH} 2>&1
```
If push fails (e.g., no upstream): set upstream:
```bash
git push --set-upstream origin ${CURRENT_BRANCH} 2>&1
```
Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)"
</step>
<step name="generate_pr_body">
Auto-generate a rich PR body from planning artifacts:
**1. Title:**
```
Phase {phase_number}: {phase_name}
```
Or for milestone: `Milestone {version}: {name}`
**2. Summary section:**
Read ROADMAP.md for phase goal. Read VERIFICATION.md for verification status.
```markdown
## Summary
**Phase {N}: {Name}**
**Goal:** {goal from ROADMAP.md}
**Status:** Verified ✓
{One paragraph synthesized from SUMMARY.md files — what was built}
```
**3. Changes section:**
For each SUMMARY.md in the phase directory:
```markdown
## Changes
### Plan {plan_id}: {plan_name}
{one_liner from SUMMARY.md frontmatter}
**Key files:**
{key-files.created and key-files.modified from SUMMARY.md frontmatter}
```
**4. Requirements section:**
```markdown
## Requirements Addressed
{REQ-IDs from plan frontmatter, linked to REQUIREMENTS.md descriptions}
```
**5. Testing section:**
```markdown
## Verification
- [x] Automated verification: {pass/fail from VERIFICATION.md}
- {human verification items from VERIFICATION.md, if any}
```
**6. Decisions section:**
```markdown
## Key Decisions
{Decisions from STATE.md accumulated context relevant to this phase}
```
</step>
<step name="create_pr">
Create the PR using the generated body:
```bash
gh pr create \
--title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
--body "${PR_BODY}" \
--base main
```
If `--draft` flag was passed: add `--draft`.
Report: "PR #{number} created: {url}"
</step>
<step name="optional_review">
Ask if user wants to trigger a code review:
```
AskUserQuestion:
question: "PR created. Run a code review before merge?"
options:
- label: "Skip review"
description: "PR is ready — merge when CI passes"
- label: "Self-review"
description: "I'll review the diff in the PR myself"
- label: "Request review"
description: "Request review from a teammate"
```
**If "Request review":**
```bash
gh pr edit ${PR_NUMBER} --add-reviewer "${REVIEWER}"
```
**If "Self-review":**
Report the PR URL and suggest: "Review the diff at {url}/files"
</step>
<step name="track_shipping">
Update STATE.md to reflect the shipping action:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)"
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}"
```
If `commit_docs` is true:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md
```
</step>
<step name="report">
```
───────────────────────────────────────────────────────────────
## ✓ Phase {X}: {Name} — Shipped
PR: #{number} ({url})
Branch: {branch} → main
Commits: {count}
Verification: ✓ Passed
Requirements: {N} REQ-IDs addressed
Next steps:
- Review/approve PR
- Merge when CI passes
- /gsd:complete-milestone (if last phase in milestone)
- /gsd:progress (to see what's next)
───────────────────────────────────────────────────────────────
```
</step>
</process>
<offer_next>
After shipping:
- /gsd:complete-milestone — if all phases in milestone are done
- /gsd:progress — see overall project state
- /gsd:execute-phase {next} — continue to next phase
</offer_next>
<success_criteria>
- [ ] Preflight checks passed (verification, clean tree, branch, remote, gh)
- [ ] Branch pushed to remote
- [ ] PR created with rich auto-generated body
- [ ] STATE.md updated with shipping status
- [ ] User knows PR number and next steps
</success_criteria>

View File

@@ -1,3 +1,19 @@
<internal_workflow>
**This is an INTERNAL workflow — NOT a user-facing command.**
There is no `/gsd:transition` command. This workflow is invoked automatically by
`execute-phase` during auto-advance, or inline by the orchestrator after phase
verification. Users should never be told to run `/gsd:transition`.
**Valid user commands for phase progression:**
- `/gsd:discuss-phase {N}` — discuss a phase before planning
- `/gsd:plan-phase {N}` — plan a phase
- `/gsd:execute-phase {N}` — execute a phase
- `/gsd:progress` — see roadmap progress
</internal_workflow>
<required_reading>
**Read these files NOW:**
@@ -58,6 +74,30 @@ cat .planning/config.json 2>/dev/null
</config-check>
**Check for verification debt in this phase:**
```bash
# Count outstanding items in current phase
OUTSTANDING=""
for f in .planning/phases/XX-current/*-UAT.md .planning/phases/XX-current/*-VERIFICATION.md; do
[ -f "$f" ] || continue
grep -q "result: pending\|result: blocked\|status: partial\|status: human_needed\|status: diagnosed" "$f" && OUTSTANDING="$OUTSTANDING\n$(basename $f)"
done
```
**If OUTSTANDING is not empty:**
Append to the completion confirmation message (regardless of mode):
```
Outstanding verification items in this phase:
{list filenames}
These will carry forward as debt. Review: `/gsd:audit-uat`
```
This does NOT block transition — it ensures the user sees the debt before confirming.
**If all plans complete:**
<if mode="yolo">

View File

@@ -20,8 +20,11 @@ First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context`
Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it.
```bash
# Runtime candidates: "<runtime>:<config-dir>"
RUNTIME_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex"
# Runtime candidates: "<runtime>:<config-dir>" stored as an array.
# Using an array instead of a space-separated string ensures correct
# iteration in both bash and zsh (zsh does not word-split unquoted
# variables by default). Fixes #1173.
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" )
# PREFERRED_RUNTIME should be set from execution_context before running this block.
# If not set, infer from runtime env vars; fallback to claude.
@@ -40,23 +43,23 @@ if [ -z "$PREFERRED_RUNTIME" ]; then
fi
# Reorder entries so preferred runtime is checked first.
ORDERED_RUNTIME_DIRS=""
for entry in $RUNTIME_DIRS; do
ORDERED_RUNTIME_DIRS=()
for entry in "${RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
ORDERED_RUNTIME_DIRS+=( "$entry" )
fi
done
for entry in $RUNTIME_DIRS; do
for entry in "${RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
ORDERED_RUNTIME_DIRS+=( "$entry" )
fi
done
# Check local first (takes priority only if valid and distinct from global)
LOCAL_VERSION_FILE="" LOCAL_MARKER_FILE="" LOCAL_DIR="" LOCAL_RUNTIME=""
for entry in $ORDERED_RUNTIME_DIRS; do
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
dir="${entry#*:}"
if [ -f "./$dir/get-shit-done/VERSION" ] || [ -f "./$dir/get-shit-done/workflows/update.md" ]; then
@@ -69,7 +72,7 @@ for entry in $ORDERED_RUNTIME_DIRS; do
done
GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME=""
for entry in $ORDERED_RUNTIME_DIRS; do
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
dir="${entry#*:}"
if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then

View File

@@ -231,6 +231,29 @@ result: skipped
reason: [user's reason if provided]
```
**If response indicates blocked:**
- "blocked", "can't test - server not running", "need physical device", "need release build"
- Or any response containing: "server", "blocked", "not running", "physical device", "release build"
Infer blocked_by tag from response:
- Contains: server, not running, gateway, API → `server`
- Contains: physical, device, hardware, real phone → `physical-device`
- Contains: release, preview, build, EAS → `release-build`
- Contains: stripe, twilio, third-party, configure → `third-party`
- Contains: depends on, prior phase, prerequisite → `prior-phase`
- Default: `other`
Update Tests section:
```
### {N}. {name}
expected: {expected}
result: blocked
blocked_by: {inferred tag}
reason: "{verbatim user response}"
```
Note: Blocked tests do NOT go into the Gaps section (they aren't code issues — they're prerequisite gates).
**If response is anything else:**
- Treat as issue description
@@ -293,8 +316,24 @@ Proceed to `present_test`.
<step name="complete_session">
**Complete testing and commit:**
**Determine final status:**
Count results:
- `pending_count`: tests with `result: [pending]`
- `blocked_count`: tests with `result: blocked`
- `skipped_no_reason`: tests with `result: skipped` and no `reason` field
```
if pending_count > 0 OR blocked_count > 0 OR skipped_no_reason > 0:
status: partial
# Session ended but not all tests resolved
else:
status: complete
# All tests have a definitive result (pass, issue, or skipped-with-reason)
```
Update frontmatter:
- status: complete
- status: {computed status}
- updated: [now]
Clear Current Test section:

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
@@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) {
// Run check in background (spawn background process, windowsHide prevents console flash)
const child = spawn(process.execPath, ['-e', `
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const cacheFile = ${JSON.stringify(cacheFile)};
@@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', `
// Check project directory first (local install), then global
let installed = '0.0.0';
let configDir = '';
try {
if (fs.existsSync(projectVersionFile)) {
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(projectVersionFile));
} else if (fs.existsSync(globalVersionFile)) {
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(globalVersionFile));
}
} catch (e) {}
// Check for stale hooks — compare hook version headers against installed VERSION
let staleHooks = [];
if (configDir) {
const hooksDir = path.join(configDir, 'hooks');
try {
if (fs.existsSync(hooksDir)) {
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js'));
for (const hookFile of hookFiles) {
try {
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (hookVersion !== installed && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
// No version header at all — definitely stale (pre-version-tracking)
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
}
} catch (e) {}
}
}
} catch (e) {}
}
let latest = null;
try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
@@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', `
update_available: latest && installed !== latest,
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000)
checked: Math.floor(Date.now() / 1000),
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
};
fs.writeFileSync(cacheFile, JSON.stringify(result));

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
@@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
let input = '';
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
// Windows/Git Bash), exit silently instead of hanging until Claude Code
// kills the process and reports "hook error". See #775.
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
// Windows/Git Bash, or slow Claude Code piping during large outputs),
// exit silently instead of hanging until Claude Code kills the process
// and reports "hook error". See #775, #1162.
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task | directory | context usage
@@ -99,6 +100,9 @@ process.stdin.on('end', () => {
if (cache.update_available) {
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
}
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
}
} catch (e) {}
}

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
// GSD Workflow Guard — PreToolUse hook
// Detects when Claude attempts file edits outside a GSD workflow context
// (no active /gsd: command or Task subagent) and injects an advisory warning.
//
// This is a SOFT guard — it advises, not blocks. The edit still proceeds.
// The warning nudges Claude to use /gsd:quick or /gsd:fast instead of
// making direct edits that bypass state tracking.
//
// Enable via config: hooks.workflow_guard: true (default: false)
// Only triggers on Write/Edit tool calls to non-.planning/ files.
const fs = require('fs');
const path = require('path');
let input = '';
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
clearTimeout(stdinTimeout);
try {
const data = JSON.parse(input);
const toolName = data.tool_name;
// Only guard Write and Edit tool calls
if (toolName !== 'Write' && toolName !== 'Edit') {
process.exit(0);
}
// Check if we're inside a GSD workflow (Task subagent or /gsd: command)
// Subagents have a session_id that differs from the parent
// and typically have a description field set by the orchestrator
if (data.tool_input?.is_subagent || data.session_type === 'task') {
process.exit(0);
}
// Check the file being edited
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
// Allow edits to .planning/ files (GSD state management)
if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
process.exit(0);
}
// Allow edits to common config/docs files that don't need GSD tracking
const allowedPatterns = [
/\.gitignore$/,
/\.env/,
/CLAUDE\.md$/,
/AGENTS\.md$/,
/GEMINI\.md$/,
/settings\.json$/,
];
if (allowedPatterns.some(p => p.test(filePath))) {
process.exit(0);
}
// Check if workflow guard is enabled
const cwd = data.cwd || process.cwd();
const configPath = path.join(cwd, '.planning', 'config.json');
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (!config.hooks?.workflow_guard) {
process.exit(0); // Guard disabled (default)
}
} catch (e) {
process.exit(0);
}
} else {
process.exit(0); // No GSD project — don't guard
}
// If we get here: GSD project, guard enabled, file edit outside .planning/,
// not in a subagent context. Inject advisory warning.
const output = {
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
'to maintain project state tracking. ' +
'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.'
}
};
process.stdout.write(JSON.stringify(output));
} catch (e) {
// Silent fail — never block tool execution
process.exit(0);
}
});

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"license": "MIT",
"bin": {
"get-shit-done-cc": "bin/install.js"

View File

@@ -1,6 +1,6 @@
{
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
"bin": {
"get-shit-done-cc": "bin/install.js"
@@ -36,7 +36,7 @@
"url": "https://github.com/glittercowboy/get-shit-done/issues"
},
"engines": {
"node": ">=16.7.0"
"node": ">=20.0.0"
},
"devDependencies": {
"c8": "^11.0.0",

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env node
/**
* Copy GSD hooks to dist for installation.
* Validates JavaScript syntax before copying to prevent shipping broken hooks.
* See #1107, #1109, #1125, #1161 — a duplicate const declaration shipped
* in dist and caused PostToolUse hook errors for all users.
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
const DIST_DIR = path.join(HOOKS_DIR, 'dist');
@@ -13,16 +17,38 @@ const DIST_DIR = path.join(HOOKS_DIR, 'dist');
const HOOKS_TO_COPY = [
'gsd-check-update.js',
'gsd-context-monitor.js',
'gsd-statusline.js'
'gsd-statusline.js',
'gsd-workflow-guard.js'
];
/**
* Validate JavaScript syntax without executing the file.
* Catches SyntaxError (duplicate const, missing brackets, etc.)
* before the hook gets shipped to users.
*/
function validateSyntax(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
try {
// Use vm.compileFunction to check syntax without executing
new vm.Script(content, { filename: path.basename(filePath) });
return null; // No error
} catch (e) {
if (e instanceof SyntaxError) {
return e.message;
}
throw e;
}
}
function build() {
// Ensure dist directory exists
if (!fs.existsSync(DIST_DIR)) {
fs.mkdirSync(DIST_DIR, { recursive: true });
}
// Copy hooks to dist
let hasErrors = false;
// Copy hooks to dist with syntax validation
for (const hook of HOOKS_TO_COPY) {
const src = path.join(HOOKS_DIR, hook);
const dest = path.join(DIST_DIR, hook);
@@ -32,9 +58,21 @@ function build() {
continue;
}
console.log(`Copying ${hook}...`);
// Validate syntax before copying
const syntaxError = validateSyntax(src);
if (syntaxError) {
console.error(`\x1b[31m✗ ${hook}: SyntaxError — ${syntaxError}\x1b[0m`);
hasErrors = true;
continue;
}
console.log(`\x1b[32m✓\x1b[0m Copying ${hook}...`);
fs.copyFileSync(src, dest);
console.log(` → ${dest}`);
}
if (hasErrors) {
console.error('\n\x1b[31mBuild failed: fix syntax errors above before publishing.\x1b[0m');
process.exit(1);
}
console.log('\nBuild complete.');

View File

@@ -151,6 +151,20 @@ describe('SPAWN: spawn type consistency', () => {
'diagnose-issues should spawn gsd-debugger, not general-purpose'
);
});
test('execute-phase has Copilot sequential fallback in runtime_compatibility', () => {
const content = fs.readFileSync(
path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'
);
assert.ok(
content.includes('sequential inline execution'),
'execute-phase must document sequential inline execution as Copilot fallback'
);
assert.ok(
content.includes('spot-check'),
'execute-phase must have spot-check fallback for completion detection'
);
});
});
// ─── Required Frontmatter Fields ─────────────────────────────────────────────
@@ -167,3 +181,34 @@ describe('AGENT: required frontmatter fields', () => {
});
}
});
// ─── Discussion Log ──────────────────────────────────────────────────────────
describe('DISCUSS: discussion log generation', () => {
test('discuss-phase workflow references DISCUSSION-LOG.md generation', () => {
const content = fs.readFileSync(
path.join(WORKFLOWS_DIR, 'discuss-phase.md'), 'utf-8'
);
assert.ok(
content.includes('DISCUSSION-LOG.md'),
'discuss-phase must reference DISCUSSION-LOG.md generation'
);
assert.ok(
content.includes('Audit trail only'),
'discuss-phase must mark discussion log as audit-only'
);
});
test('discussion-log template exists', () => {
const templatePath = path.join(__dirname, '..', 'get-shit-done', 'templates', 'discussion-log.md');
assert.ok(
fs.existsSync(templatePath),
'discussion-log.md template must exist'
);
const content = fs.readFileSync(templatePath, 'utf-8');
assert.ok(
content.includes('Do not use as input to planning'),
'template must contain audit-only notice'
);
});
});

82
tests/claude-md.test.cjs Normal file
View File

@@ -0,0 +1,82 @@
/**
* CLAUDE.md generation and new-project workflow tests
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
describe('generate-claude-md', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('creates CLAUDE.md with workflow enforcement section', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.action, 'created');
assert.strictEqual(output.sections_total, 5);
assert.ok(output.sections_generated.includes('workflow'));
const claudePath = path.join(tmpDir, 'CLAUDE.md');
const content = fs.readFileSync(claudePath, 'utf-8');
assert.ok(content.includes('## GSD Workflow Enforcement'));
assert.ok(content.includes('/gsd:quick'));
assert.ok(content.includes('/gsd:debug'));
assert.ok(content.includes('/gsd:execute-phase'));
assert.ok(content.includes('Do not make direct repo edits outside a GSD workflow'));
});
test('adds workflow enforcement section when updating an existing CLAUDE.md', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '## Local Notes\n\nKeep this intro.\n');
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.action, 'updated');
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('## Local Notes'));
assert.ok(content.includes('## GSD Workflow Enforcement'));
});
});
describe('new-project workflow includes CLAUDE.md generation', () => {
const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'new-project.md');
const commandsPath = path.join(__dirname, '..', 'docs', 'COMMANDS.md');
test('new-project workflow generates CLAUDE.md before final commit', () => {
const content = fs.readFileSync(workflowPath, 'utf-8');
assert.ok(content.includes('generate-claude-md'));
assert.ok(content.includes('--files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md'));
});
test('new-project artifacts mention CLAUDE.md', () => {
const workflowContent = fs.readFileSync(workflowPath, 'utf-8');
const commandsContent = fs.readFileSync(commandsPath, 'utf-8');
assert.ok(workflowContent.includes('| Project guide | `CLAUDE.md`'));
assert.ok(workflowContent.includes('- `CLAUDE.md`'));
assert.ok(commandsContent.includes('`CLAUDE.md`'));
});
});

View File

@@ -160,6 +160,19 @@ tools: Read, Grep, Glob
assert.ok(result.includes("'''"), 'has closing literal triple quotes');
});
test('includes required name and description fields', () => {
const result = generateCodexAgentToml('gsd-executor', sampleAgent);
assert.ok(result.includes('name = "gsd-executor"'), 'has name');
assert.ok(result.includes('description = "Executes plans"'), 'has description');
});
test('falls back to generated description when frontmatter is missing fields', () => {
const minimalAgent = `<role>You are an unknown agent.</role>`;
const result = generateCodexAgentToml('gsd-unknown', minimalAgent);
assert.ok(result.includes('name = "gsd-unknown"'), 'falls back to agent name');
assert.ok(result.includes('description = "GSD agent gsd-unknown"'), 'falls back to synthetic description');
});
test('defaults unknown agents to read-only', () => {
const result = generateCodexAgentToml('gsd-unknown', sampleAgent);
assert.ok(result.includes('sandbox_mode = "read-only"'), 'defaults to read-only');
@@ -354,6 +367,36 @@ describe('mergeCodexConfig', () => {
assert.ok(content.includes('[agents.gsd-executor]'), 'has agent');
});
test('case 3 strips existing [agents.gsd-*] sections before appending fresh block', () => {
const configPath = path.join(tmpDir, 'config.toml');
const existing = [
'[model]',
'name = "o3"',
'',
'[agents.custom-agent]',
'description = "user agent"',
'',
'',
'[agents.gsd-executor]',
'description = "old"',
'config_file = "agents/gsd-executor.toml"',
'',
].join('\n');
fs.writeFileSync(configPath, existing);
mergeCodexConfig(configPath, sampleBlock);
const content = fs.readFileSync(configPath, 'utf8');
const gsdAgentCount = (content.match(/^\[agents\.gsd-executor\]\s*$/gm) || []).length;
const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
assert.ok(content.includes('[model]'), 'preserves user content');
assert.ok(content.includes('[agents.custom-agent]'), 'preserves non-GSD agent section');
assert.strictEqual(gsdAgentCount, 1, 'keeps exactly one GSD agent section');
assert.strictEqual(markerCount, 1, 'adds exactly one marker block');
assert.ok(!/\n{3,}# GSD Agent Configuration/.test(content), 'does not leave extra blank lines before marker block');
});
test('idempotent: re-merge produces same result', () => {
const configPath = path.join(tmpDir, 'config.toml');
mergeCodexConfig(configPath, sampleBlock);
@@ -485,10 +528,47 @@ describe('installCodexConfig (integration)', () => {
assert.ok(fs.existsSync(path.join(agentsDir, 'gsd-plan-checker.toml')), 'plan-checker .toml exists');
const executorToml = fs.readFileSync(path.join(agentsDir, 'gsd-executor.toml'), 'utf8');
assert.ok(executorToml.includes('name = "gsd-executor"'), 'executor has name');
assert.ok(executorToml.includes('description = "Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command."'), 'executor has description');
assert.ok(executorToml.includes('sandbox_mode = "workspace-write"'), 'executor is workspace-write');
assert.ok(executorToml.includes('developer_instructions'), 'has developer_instructions');
const checkerToml = fs.readFileSync(path.join(agentsDir, 'gsd-plan-checker.toml'), 'utf8');
assert.ok(checkerToml.includes('name = "gsd-plan-checker"'), 'plan-checker has name');
assert.ok(checkerToml.includes('sandbox_mode = "read-only"'), 'plan-checker is read-only');
});
});
// ─── Codex config.toml [features] safety (#1202) ─────────────────────────────
describe('codex features section safety', () => {
test('non-boolean keys under [features] are moved to top level', () => {
// Simulate the bug from #1202: model = "gpt-5.4" under [features]
// causes "invalid type: string, expected a boolean in features"
const configContent = `[features]\ncodex_hooks = true\n\nmodel = "gpt-5.4"\nmodel_reasoning_effort = "medium"\n\n[agents.gsd-executor]\ndescription = "test"\n`;
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
assert.ok(featuresMatch, 'features section found');
const featuresBody = featuresMatch[1];
const nonBooleanKeys = featuresBody.split('\n')
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
.map(line => line.trim());
assert.strictEqual(nonBooleanKeys.length, 2, 'should detect 2 non-boolean keys');
assert.ok(nonBooleanKeys.includes('model = "gpt-5.4"'), 'detects model key');
assert.ok(nonBooleanKeys.includes('model_reasoning_effort = "medium"'), 'detects model_reasoning_effort key');
});
test('boolean keys under [features] are NOT flagged', () => {
const configContent = `[features]\ncodex_hooks = true\nmulti_agent = false\n`;
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
const featuresBody = featuresMatch[1];
const nonBooleanKeys = featuresBody.split('\n')
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
.map(line => line.trim());
assert.strictEqual(nonBooleanKeys.length, 0, 'no non-boolean keys in a clean config');
});
});

View File

@@ -363,6 +363,37 @@ requirements-completed:
assert.strictEqual(output.decisions, undefined, 'decisions excluded');
});
test('extracts one-liner from body when not in frontmatter', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(
path.join(phaseDir, '01-01-SUMMARY.md'),
`---
phase: "01"
key-files:
- src/lib/db.ts
---
# Phase 1: Foundation Summary
**JWT auth with refresh rotation using jose library**
## Performance
- **Duration:** 28 min
- **Tasks:** 5
`
);
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.one_liner, 'JWT auth with refresh rotation using jose library',
'one-liner should be extracted from body **bold** line');
});
test('handles missing frontmatter fields gracefully', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
@@ -567,6 +598,98 @@ describe('todo complete command', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// todo match-phase command
// ─────────────────────────────────────────────────────────────────────────────
describe('todo match-phase command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => cleanup(tmpDir));
test('returns empty matches when no todos exist', () => {
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
assert.strictEqual(output.todo_count, 0);
assert.deepStrictEqual(output.matches, []);
});
test('matches todo by keyword overlap with phase name', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nNeed to handle token expiry for OAuth flows.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
assert.strictEqual(output.todo_count, 1, 'should find 1 todo');
assert.ok(output.matches.length > 0, 'should have matches');
assert.strictEqual(output.matches[0].title, 'Add OAuth token refresh');
assert.ok(output.matches[0].score > 0, 'score should be positive');
assert.ok(output.matches[0].reasons.length > 0, 'should have reasons');
});
test('does not match unrelated todo', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token expiry.');
fs.writeFileSync(path.join(pendingDir, 'unrelated-todo.md'),
'title: Fix CSS grid layout in dashboard\narea: ui\ncreated: 2026-03-01\n\nGrid columns break on mobile.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
assert.ok(result.success, 'should succeed');
const output = JSON.parse(result.output);
const matchTitles = output.matches.map(m => m.title);
assert.ok(matchTitles.includes('Add OAuth token refresh'), 'auth todo should match');
assert.ok(!matchTitles.includes('Fix CSS grid layout in dashboard'), 'unrelated todo should not match');
});
test('matches todo by area overlap', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token handling.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Auth System\n\n**Goal:** Build auth module\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
const output = JSON.parse(result.output);
const authMatch = output.matches.find(m => m.title === 'Add OAuth token refresh');
assert.ok(authMatch, 'should find auth todo');
const hasAreaReason = authMatch.reasons.some(r => r.startsWith('area:'));
assert.ok(hasAreaReason, 'should match on area');
});
test('sorts matches by score descending', () => {
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
fs.mkdirSync(pendingDir, { recursive: true });
fs.writeFileSync(path.join(pendingDir, 'weak-match.md'),
'title: Check token format\narea: general\ncreated: 2026-03-01\n\nToken format validation.');
fs.writeFileSync(path.join(pendingDir, 'strong-match.md'),
'title: Session management authentication OAuth token handling\narea: auth\ncreated: 2026-03-01\n\nSession auth OAuth tokens.');
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login, session handling, and token management\n');
const result = runGsdTools('todo match-phase 01', tmpDir);
const output = JSON.parse(result.output);
assert.ok(output.matches.length >= 2, 'should have multiple matches');
for (let i = 1; i < output.matches.length; i++) {
assert.ok(output.matches[i - 1].score >= output.matches[i].score,
`match ${i-1} score (${output.matches[i-1].score}) should be >= match ${i} score (${output.matches[i].score})`);
}
});
});
// ─────────────────────────────────────────────────────────────────────────────
// scaffold command
// ─────────────────────────────────────────────────────────────────────────────

Some files were not shown because too many files have changed in this diff Show More