diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94fb64bb4..efda73dff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.release-monitor.sh b/.release-monitor.sh new file mode 100755 index 000000000..200383398 --- /dev/null +++ b/.release-monitor.sh @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f7d2a69..adebc2e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ddcee19a7..48b8fc7e6 100644 --- a/README.md +++ b/README.md @@ -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 --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 ` | 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`. diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 03f604248..9b818becd 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -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. diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 7b897903c..1a767b9c8 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -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) @@ -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} | ``` @@ -556,4 +591,4 @@ Quality indicators: - **Actionable:** Planner could create tasks based on this research - **Current:** Year included in searches, publication dates checked - + \ No newline at end of file diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..b05cb6d86 100755 --- a/bin/install.js +++ b/bin/install.js @@ -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 ${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 ${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 ` +## 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") +`; +} + +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 text 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>/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 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, diff --git a/commands/gsd/add-backlog.md b/commands/gsd/add-backlog.md new file mode 100644 index 000000000..a144fb975 --- /dev/null +++ b/commands/gsd/add-backlog.md @@ -0,0 +1,76 @@ +--- +name: gsd:add-backlog +description: Add an idea to the backlog parking lot (999.x numbering) +argument-hint: +allowed-tools: + - Read + - Write + - Bash +--- + + +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. + + + + +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. + ``` + + + + +- 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 + diff --git a/commands/gsd/audit-uat.md b/commands/gsd/audit-uat.md new file mode 100644 index 000000000..604e1be12 --- /dev/null +++ b/commands/gsd/audit-uat.md @@ -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 +--- + +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. + + + +@~/.claude/get-shit-done/workflows/audit-uat.md + + + +Core planning files are loaded in-workflow via CLI. + +**Scope:** +Glob: .planning/phases/*/*-UAT.md +Glob: .planning/phases/*/*-VERIFICATION.md + diff --git a/commands/gsd/discuss-phase.md b/commands/gsd/discuss-phase.md index b5c926021..75ebde603 100644 --- a/commands/gsd/discuss-phase.md +++ b/commands/gsd/discuss-phase.md @@ -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: " [--auto]" +argument-hint: " [--auto] [--batch] [--analyze]" allowed-tools: - Read - Write diff --git a/commands/gsd/execute-phase.md b/commands/gsd/execute-phase.md index 1a798471f..b0741db0c 100644 --- a/commands/gsd/execute-phase.md +++ b/commands/gsd/execute-phase.md @@ -1,7 +1,7 @@ --- name: gsd:execute-phase description: Execute all plans in a phase with wave-based parallelization -argument-hint: " [--gaps-only]" +argument-hint: " [--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 `` blocks. diff --git a/commands/gsd/fast.md b/commands/gsd/fast.md new file mode 100644 index 000000000..4121f9f75 --- /dev/null +++ b/commands/gsd/fast.md @@ -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 +--- + + +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. + + + +@~/.claude/get-shit-done/workflows/fast.md + + + +Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end. + diff --git a/commands/gsd/next.md b/commands/gsd/next.md new file mode 100644 index 000000000..e7d81c747 --- /dev/null +++ b/commands/gsd/next.md @@ -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 +--- + +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. + + + +@~/.claude/get-shit-done/workflows/next.md + + + +Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end. + diff --git a/commands/gsd/plant-seed.md b/commands/gsd/plant-seed.md new file mode 100644 index 000000000..95ab4efc8 --- /dev/null +++ b/commands/gsd/plant-seed.md @@ -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 +--- + + +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) + + + +@~/.claude/get-shit-done/workflows/plant-seed.md + + + +Execute the plant-seed workflow from @~/.claude/get-shit-done/workflows/plant-seed.md end-to-end. + diff --git a/commands/gsd/pr-branch.md b/commands/gsd/pr-branch.md new file mode 100644 index 000000000..6c2d7f8d9 --- /dev/null +++ b/commands/gsd/pr-branch.md @@ -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 +--- + + +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. + + + +@~/.claude/get-shit-done/workflows/pr-branch.md + + + +Execute the pr-branch workflow from @~/.claude/get-shit-done/workflows/pr-branch.md end-to-end. + diff --git a/commands/gsd/review-backlog.md b/commands/gsd/review-backlog.md new file mode 100644 index 000000000..91de5dd83 --- /dev/null +++ b/commands/gsd/review-backlog.md @@ -0,0 +1,61 @@ +--- +name: gsd:review-backlog +description: Review and promote backlog items to active milestone +allowed-tools: + - Read + - Write + - Bash +--- + + +Review all 999.x backlog items and optionally promote them into the active +milestone sequence or remove stale entries. + + + + +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} + ``` + + diff --git a/commands/gsd/review.md b/commands/gsd/review.md new file mode 100644 index 000000000..84d8171f6 --- /dev/null +++ b/commands/gsd/review.md @@ -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 +--- + + +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 + + + +@~/.claude/get-shit-done/workflows/review.md + + + +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 + + + +Execute the review workflow from @~/.claude/get-shit-done/workflows/review.md end-to-end. + diff --git a/commands/gsd/session-report.md b/commands/gsd/session-report.md new file mode 100644 index 000000000..a0eb1d6ef --- /dev/null +++ b/commands/gsd/session-report.md @@ -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 +--- + +Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review. + + + +@~/.claude/get-shit-done/workflows/session-report.md + + + +Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end. + diff --git a/commands/gsd/ship.md b/commands/gsd/ship.md new file mode 100644 index 000000000..124695553 --- /dev/null +++ b/commands/gsd/ship.md @@ -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 +--- + +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. + + + +@~/.claude/get-shit-done/workflows/ship.md + + +Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end. diff --git a/commands/gsd/thread.md b/commands/gsd/thread.md new file mode 100644 index 000000000..fe921184b --- /dev/null +++ b/commands/gsd/thread.md @@ -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 +--- + + +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. + + + + +**Parse $ARGUMENTS to determine mode:** + + +**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 +``` + + + +**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`. + + + +**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} + ``` + + + + + +- 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 + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0163b0cc0..f54ce8816 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index f0228496b..2e1dcfb0f 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -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 +# UAT audit — scan all phases for unresolved items +node gsd-tools.cjs audit-uat + # Git commit with config checks -node gsd-tools.cjs commit [--files f1 f2] [--amend] +node gsd-tools.cjs commit [--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 [--limit N] [--freshness day|week|month] @@ -355,3 +361,6 @@ node gsd-tools.cjs websearch [--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 | diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..924f15bf9 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -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 ``` --- diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d1631752..7836f3edc 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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) | --- diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 27afed54e..cce53eb26 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -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 ` @@ -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 diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5458cedc0..ba7b1abf8 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -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 ` | 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 diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md new file mode 100644 index 000000000..150aa346e --- /dev/null +++ b/docs/zh-CN/README.md @@ -0,0 +1,707 @@ +
+ +# 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) + +
+ +```bash +npx get-shit-done-cc@latest +``` + +**支持 Mac、Windows 和 Linux。** + +
+ +![GSD Install](../assets/terminal.svg) + +
+ +*"如果你清楚自己想要什么,它真的会帮你构建出来。不忽悠。"* + +*"我试过 SpecKit、OpenSpec 和 Taskmaster —— 这是我用过的效果最好的。"* + +*"这是我用过的 Claude Code 最强大的扩展。没有过度设计。真的就是把事情做完。"* + +
+ +**被 Amazon、Google、Shopify 和 Webflow 的工程师信赖使用。** + +[我为什么开发这个](#我为什么开发这个) · [工作原理](#工作原理) · [命令](#命令) · [为什么有效](#为什么有效) · [用户指南](USER-GUIDE.md) + +
+ +--- + +## 我为什么开发这个 + +我是一名独立开发者。我不写代码 —— 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 +``` + +
+非交互式安装(Docker、CI、脚本) + +```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` 跳过运行时提示。 + +
+ +
+开发安装 + +克隆仓库并本地运行安装程序: + +```bash +git clone https://github.com/glittercowboy/get-shit-done.git +cd get-shit-done +node bin/install.js --claude --local +``` + +安装到 `./.claude/` 用于在贡献前测试修改。 + +
+ +### 推荐:跳过权限模式 + +GSD 设计为无摩擦自动化。运行 Claude Code 时使用: + +```bash +claude --dangerously-skip-permissions +``` + +> [!TIP] +> 这是 GSD 的预期使用方式 —— 停下来 50 次批准 `date` 和 `git commit` 会失去意义。 + +
+替代方案:细粒度权限 + +如果你不想使用那个标志,在项目的 `.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:*)" + ] + } +} +``` + +
+ +--- + +## 工作原理 + +> **已有代码?** 先运行 `/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 --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 + + 创建登录端点 + src/app/api/auth/login/route.ts + + 使用 jose 处理 JWT(不用 jsonwebtoken - CommonJS 问题)。 + 根据 users 表验证凭据。 + 成功时返回 httpOnly cookie。 + + curl -X POST localhost:3000/api/auth/login 返回 200 + Set-Cookie + 有效凭据返回 cookie,无效返回 401 + +``` + +精确的指令。不猜测。内置验证。 + +### 多代理编排 + +每个阶段使用相同模式:轻量编排器生成专门代理,收集结果,路由到下一步。 + +| 阶段 | 编排器做 | 代理做 | +|-------|------------------|-----------| +| 研究 | 协调,呈现发现 | 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 ` | 在并行波次中执行所有计划,完成后验证 | +| `/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 ` | 切换模型配置文件(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` 自动修复 | + +¹ 由 Reddit 用户 OracleGreyBeard 贡献 + +--- + +## 配置 + +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 历史 + + + + + + Star History Chart + + + +--- + +## 许可证 + +MIT 许可证。详见 [LICENSE](../LICENSE)。 + +--- + +
+ +**Claude Code 很强大。GSD 让它可靠。** + +
\ No newline at end of file diff --git a/docs/zh-CN/USER-GUIDE.md b/docs/zh-CN/USER-GUIDE.md new file mode 100644 index 000000000..66cae6a0a --- /dev/null +++ b/docs/zh-CN/USER-GUIDE.md @@ -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 ` | 在并行波次中执行所有计划 | 规划完成后 | +| `/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 ` | 快速切换配置 | 更改成本/质量权衡 | +| `/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 # 执行后验证结果 +``` \ No newline at end of file diff --git a/docs/zh-CN/references/checkpoints.md b/docs/zh-CN/references/checkpoints.md new file mode 100644 index 000000000..a41a22edb --- /dev/null +++ b/docs/zh-CN/references/checkpoints.md @@ -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 + + [Claude 自动化并部署/构建的内容] + + [测试的确切步骤 - URL、命令、预期行为] + + [如何继续 - "approved"、"yes" 或描述问题] + +``` + +**示例:UI 组件(展示关键模式:Claude 在检查点之前启动服务器)** +```xml + + 构建响应式仪表板布局 + src/components/Dashboard.tsx, src/app/dashboard/page.tsx + 创建带侧边栏、标题和内容区域的仪表板。使用 Tailwind 响应式类处理移动端。 + npm run build 成功,无 TypeScript 错误 + 仪表板组件构建无错误 + + + + 启动开发服务器用于验证 + 在后台运行 `npm run dev`,等待 "ready" 消息,捕获端口 + curl http://localhost:3000 返回 200 + 开发服务器运行于 http://localhost:3000 + + + + 响应式仪表板布局 - 开发服务器运行于 http://localhost:3000 + + 访问 http://localhost:3000/dashboard 并验证: + 1. 桌面端 (>1024px): 左侧边栏,右侧内容,顶部标题 + 2. 平板端 (768px): 侧边栏折叠为汉堡菜单 + 3. 移动端 (375px): 单列布局,出现底部导航 + 4. 任何尺寸无布局偏移或水平滚动 + + 输入 "approved" 或描述布局问题 + +``` + +### checkpoint:decision(9%) + +**何时使用:** 人工必须做出影响实现方向的选择。 + +**用于:** +- 技术选型(哪个认证提供商、哪个数据库) +- 架构决策(monorepo 还是独立仓库) +- 设计选择(配色方案、布局方式) +- 功能优先级(构建哪个变体) +- 数据模型决策(模式结构) + +**结构:** +```xml + + [正在决策的内容] + [为什么这个决策重要] + + + + + [如何表明选择] + +``` + +**示例:认证提供商选择** +```xml + + 选择认证提供商 + + 应用需要用户认证。三个可靠选项各有权衡。 + + + + + + + 选择:supabase、clerk 或 nextauth + +``` + +### checkpoint:human-action(1% - 罕见) + +**何时使用:** 操作没有 CLI/API 且需要仅人工交互,或者 Claude 在自动化过程中遇到认证门控。 + +**仅用于:** +- **认证门控** - Claude 尝试了 CLI/API 但需要凭证(这不是失败) +- 邮箱验证链接(点击邮件) +- 短信两步验证码(手机验证) +- 人工账户审批(平台需要人工审核) +- 信用卡 3D Secure 流程(基于 Web 的支付授权) +- OAuth 应用审批(基于 Web 的审批) + +**不要用于预定的手动工作:** +- 部署(使用 CLI - 如需要则认证门控) +- 创建 webhooks/数据库(使用 API/CLI - 如需要则认证门控) +- 运行构建/测试(使用 Bash 工具) +- 创建文件(使用 Write 工具) + +**结构:** +```xml + + [人工必须做什么 - Claude 已完成所有可自动化的] + + [Claude 已自动化的内容] + [需要人工操作的一件事] + + [Claude 之后可以检查的内容] + [如何继续] + +``` + +**示例:认证门控(动态检查点)** +```xml + + 部署到 Vercel + .vercel/, vercel.json + 运行 `vercel --yes` 进行部署 + vercel ls 显示部署,curl 返回 200 + + + + + + 认证 Vercel CLI 以便我继续部署 + + 我尝试部署但收到认证错误。 + 运行:vercel login + 这将打开你的浏览器 - 完成认证流程。 + + vercel whoami 返回你的账户邮箱 + 认证完成后输入 "done" + + + + + + 重试 Vercel 部署 + 运行 `vercel --yes`(已认证) + vercel ls 显示部署,curl 返回 200 + +``` + +**关键区别:** 认证门控是 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 + + 仪表板组件 + + 1. 运行: npm run dev + 2. 访问: http://localhost:3000/dashboard + 3. 检查布局是否正确 + + +``` +**为什么错误:** Claude 可以运行 `npm run dev`。用户应该只访问 URL,不执行命令。 + +### ✅ 正确:Claude 启动服务器,用户访问 +```xml + + 启动开发服务器 + 在后台运行 `npm run dev` + curl localhost:3000 返回 200 + + + + http://localhost:3000/dashboard 的仪表板(服务器运行中) + + 访问 http://localhost:3000/dashboard 并验证: + 1. 布局匹配设计 + 2. 无控制台错误 + + +``` + +### ❌ 错误:让用户部署 / ✅ 正确:Claude 自动化 +```xml + + + 部署到 Vercel + 访问 vercel.com/new → 导入仓库 → 点击部署 → 复制 URL + + + + + 部署到 Vercel + 运行 `vercel --yes`。捕获 URL。 + vercel ls 显示部署,curl 返回 200 + + + + 已部署到 {url} + 访问 {url},检查首页加载 + 输入 "approved" + +``` + +## 摘要 + +检查点规范化人工介入点用于验证和决策,而非手动工作。 + +**黄金法则:** 如果 Claude 能自动化它,Claude 就必须自动化它。 + +**检查点优先级:** +1. **checkpoint:human-verify**(90%)- Claude 自动化一切,人工确认视觉/功能正确性 +2. **checkpoint:decision**(9%)- 人工做出架构/技术选择 +3. **checkpoint:human-action**(1%)- 真正无法避免的、没有 API/CLI 的手动步骤 + +**何时不用检查点:** +- Claude 可以编程验证的事情(测试、构建) +- 文件操作(Claude 可以读取文件) +- 代码正确性(测试和静态分析) +- 任何可通过 CLI/API 自动化的内容 \ No newline at end of file diff --git a/docs/zh-CN/references/continuation-format.md b/docs/zh-CN/references/continuation-format.md new file mode 100644 index 000000000..674b3e770 --- /dev/null +++ b/docs/zh-CN/references/continuation-format.md @@ -0,0 +1,249 @@ +# 续接格式 + +完成命令或工作流后展示下一步的标准格式。 + +## 核心结构 + +``` +--- + +## ▶ 下一步 + +**{标识符}: {名称}** — {单行描述} + +`{可复制粘贴的命令}` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `{备选项 1}` — 描述 +- `{备选项 2}` — 描述 + +--- +``` + +## 格式规则 + +1. **始终展示它是什么** — 名称 + 描述,绝不仅仅是一个命令路径 +2. **从源文件拉取上下文** — ROADMAP.md 用于阶段,PLAN.md `` 用于计划 +3. **命令用内联代码** — 反引号,易于复制粘贴,渲染为可点击链接 +4. **`/clear` 说明** — 始终包含,保持简洁但解释原因 +5. **用"也可选"而非"其他选项"** — 听起来更像应用 +6. **视觉分隔符** — 上下用 `---` 使其突出 + +## 变体 + +### 执行下一个计划 + +``` +--- + +## ▶ 下一步 + +**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh + +`/gsd:execute-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- 执行前审查计划 +- `/gsd:list-phase-assumptions 2` — 检查假设 + +--- +``` + +### 执行阶段中最后一个计划 + +添加注释说明这是最后一个计划以及接下来是什么: + +``` +--- + +## ▶ 下一步 + +**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh +阶段 2 的最后一个计划 + +`/gsd:execute-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**完成后:** +- 阶段 2 → 阶段 3 过渡 +- 下一步:**阶段 3: 核心功能** — 用户仪表板和设置 + +--- +``` + +### 规划阶段 + +``` +--- + +## ▶ 下一步 + +**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程 + +`/gsd:plan-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `/gsd:discuss-phase 2` — 先收集上下文 +- `/gsd:research-phase 2` — 调查未知项 +- 审查路线图 + +--- +``` + +### 阶段完成,准备下一步 + +在下一步操作前显示完成状态: + +``` +--- + +## ✓ 阶段 2 完成 + +3/3 计划已执行 + +## ▶ 下一步 + +**阶段 3: 核心功能** — 用户仪表板、设置和数据导出 + +`/gsd:plan-phase 3` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `/gsd:discuss-phase 3` — 先收集上下文 +- `/gsd:research-phase 3` — 调查未知项 +- 回顾阶段 2 构建的内容 + +--- +``` + +### 多个同等选项 + +当没有明确的主要操作时: + +``` +--- + +## ▶ 下一步 + +**阶段 3: 核心功能** — 用户仪表板、设置和数据导出 + +**直接规划:** `/gsd:plan-phase 3` + +**先讨论上下文:** `/gsd:discuss-phase 3` + +**研究未知项:** `/gsd:research-phase 3` + +`/clear` 优先 → 全新上下文窗口 + +--- +``` + +### 里程碑完成 + +``` +--- + +## 🎉 里程碑 v1.0 完成 + +全部 4 个阶段已发布 + +## ▶ 下一步 + +**开始 v1.1** — 提问 → 研究 → 需求 → 路线图 + +`/gsd:new-milestone` + +`/clear` 优先 → 全新上下文窗口 + +--- +``` + +## 拉取上下文 + +### 用于阶段(从 ROADMAP.md): + +```markdown +### 阶段 2: 认证 +**目标**: 带刷新令牌的 JWT 登录流程 +``` + +提取:`**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程` + +### 用于计划(从 ROADMAP.md): + +```markdown +计划: +- [ ] 02-03: 添加刷新令牌轮换 +``` + +或从 PLAN.md ``: + +```xml + +添加带滑动过期窗口的刷新令牌轮换。 + +目的: 在不影响安全性的前提下延长会话生命周期。 + +``` + +提取:`**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh` + +## 反模式 + +### 不要:仅命令(无上下文) + +``` +## 继续 + +运行 `/clear`,然后粘贴: +/gsd:execute-phase 2 +``` + +用户不知道 02-03 是关于什么的。 + +### 不要:缺少 /clear 说明 + +``` +`/gsd:plan-phase 3` + +先运行 /clear。 +``` + +没有解释原因。用户可能跳过。 + +### 不要:"其他选项" 措辞 + +``` +其他选项: +- 审查路线图 +``` + +听起来像是事后补充。用"也可选:"替代。 + +### 不要:用围栏代码块展示命令 + +``` +``` +/gsd:plan-phase 3 +``` +``` + +模板内的围栏代码块会造成嵌套歧义。用内联反引号替代。 \ No newline at end of file diff --git a/docs/zh-CN/references/decimal-phase-calculation.md b/docs/zh-CN/references/decimal-phase-calculation.md new file mode 100644 index 000000000..d25f46870 --- /dev/null +++ b/docs/zh-CN/references/decimal-phase-calculation.md @@ -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/` \ No newline at end of file diff --git a/docs/zh-CN/references/git-integration.md b/docs/zh-CN/references/git-integration.md new file mode 100644 index 000000000..8fb58d0a6 --- /dev/null +++ b/docs/zh-CN/references/git-integration.md @@ -0,0 +1,248 @@ + +GSD 框架的 Git 集成。 + + + + +**提交结果,而非过程。** + +git 日志应该读起来像是发布内容的变更日志,而不是规划活动的日记。 + + + + +| 事件 | 提交? | 原因 | +| ----------------------- | ------- | ------------------------------------------------ | +| BRIEF + ROADMAP 创建 | 是 | 项目初始化 | +| PLAN.md 创建 | 否 | 中间产物 - 与计划完成一起提交 | +| RESEARCH.md 创建 | 否 | 中间产物 | +| DISCOVERY.md 创建 | 否 | 中间产物 | +| **任务完成** | 是 | 原子工作单元(每个任务 1 个提交) | +| **计划完成** | 是 | 元数据提交(SUMMARY + STATE + ROADMAP) | +| 交接创建 | 是 | WIP 状态保留 | + + + + + +```bash +[ -d .git ] && echo "GIT_EXISTS" || echo "NO_GIT" +``` + +如果 NO_GIT:静默运行 `git init`。GSD 项目总是有自己的仓库。 + + + + + +## 项目初始化(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/ +``` + + + + +## 任务完成(计划执行期间) + +每个任务在完成后立即获得自己的提交。 + +``` +{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 +" +``` + + + + +## 计划完成(所有任务完成后) + +所有任务提交后,最后一个元数据提交捕获计划完成。 + +``` +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 +``` + +**注意:** 代码文件不包含 - 已按任务提交。 + + + + +## 交接(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/ +``` + + + + + + +**旧方法(每个计划提交):** +``` +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。 + + + + + +**仍不要提交(中间产物):** +- PLAN.md 创建(与计划完成一起提交) +- RESEARCH.md(中间产物) +- DISCOVERY.md(中间产物) +- 小的规划调整 +- "Fixed typo in roadmap" + +**要提交(结果):** +- 每个任务完成(feat/fix/test/refactor) +- 计划完成元数据(docs) +- 项目初始化(docs) + +**关键原则:** 提交可工作的代码和已发布的结果,而非规划过程。 + + + + + +## 为什么使用每任务提交? + +**AI 上下文工程:** +- Git 历史成为未来 Claude 会话的主要上下文源 +- `git log --grep="{phase}-{plan}"` 显示计划的所有工作 +- `git diff ^..` 显示每个任务的确切变更 +- 减少对解析 SUMMARY.md 的依赖 = 更多上下文用于实际工作 + +**失败恢复:** +- 任务 1 已提交 ✅,任务 2 失败 ❌ +- 下次会话中的 Claude:看到任务 1 完成,可以重试任务 2 +- 可以 `git reset --hard` 到最后一个成功的任务 + +**调试:** +- `git bisect` 找到确切的失败任务,而不仅仅是失败计划 +- `git blame` 将行追溯到特定任务上下文 +- 每个提交独立可回滚 + +**可观察性:** +- 独立开发者 + Claude 工作流受益于细粒度归因 +- 原子提交是 git 最佳实践 +- 当消费者是 Claude 而非人类时,"提交噪音"无关紧要 + + \ No newline at end of file diff --git a/docs/zh-CN/references/git-planning-commit.md b/docs/zh-CN/references/git-planning-commit.md new file mode 100644 index 000000000..fd128ba6c --- /dev/null +++ b/docs/zh-CN/references/git-planning-commit.md @@ -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/` 检查) \ No newline at end of file diff --git a/docs/zh-CN/references/model-profile-resolution.md b/docs/zh-CN/references/model-profile-resolution.md new file mode 100644 index 000000000..777a9a4b0 --- /dev/null +++ b/docs/zh-CN/references/model-profile-resolution.md @@ -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"`) \ No newline at end of file diff --git a/docs/zh-CN/references/model-profiles.md b/docs/zh-CN/references/model-profiles.md new file mode 100644 index 000000000..011fbba57 --- /dev/null +++ b/docs/zh-CN/references/model-profiles.md @@ -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 ` + +项目默认值:在 `.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。 \ No newline at end of file diff --git a/docs/zh-CN/references/phase-argument-parsing.md b/docs/zh-CN/references/phase-argument-parsing.md new file mode 100644 index 000000000..1434143bb --- /dev/null +++ b/docs/zh-CN/references/phase-argument-parsing.md @@ -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) +``` \ No newline at end of file diff --git a/docs/zh-CN/references/planning-config.md b/docs/zh-CN/references/planning-config.md new file mode 100644 index 000000000..075d2f85d --- /dev/null +++ b/docs/zh-CN/references/planning-config.md @@ -0,0 +1,200 @@ + + +`.planning/` 目录行为的配置选项。 + + +```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}"` | 里程碑策略的分支模板 | + + + + +**当 `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 状态 —— 无需手动条件判断。 + + + + + +**当 `search_gitignored: false`(默认):** +- 标准 rg 行为(尊重 .gitignore) +- 直接路径搜索有效:`rg "pattern" .planning/` 找到文件 +- 广泛搜索跳过 gitignored:`rg "pattern"` 跳过 `.planning/` + +**当 `search_gitignored: true`:** +- 在应该包含 `.planning/` 的广泛 rg 搜索中添加 `--no-ignore` +- 仅在搜索整个仓库并期望 `.planning/` 匹配时需要 + +**注意:** 大多数 GSD 操作使用直接文件读取或显式路径,无论 gitignore 状态如何都有效。 + + + + + +使用未提交模式: + +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/` 文件,然后才进行合并提交。 + + + + + +**分支策略:** + +| 策略 | 创建分支时机 | 分支范围 | 合并点 | +|----------|---------------------|--------------|-------------| +| `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 | + + + + \ No newline at end of file diff --git a/docs/zh-CN/references/questioning.md b/docs/zh-CN/references/questioning.md new file mode 100644 index 000000000..7f23dbe07 --- /dev/null +++ b/docs/zh-CN/references/questioning.md @@ -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 来构建。 \ No newline at end of file diff --git a/docs/zh-CN/references/tdd.md b/docs/zh-CN/references/tdd.md new file mode 100644 index 000000000..247bf77d3 --- /dev/null +++ b/docs/zh-CN/references/tdd.md @@ -0,0 +1,263 @@ + +TDD 关乎设计质量,而非覆盖率指标。红-绿-重构循环迫使你在实现前思考行为,从而产生更清晰的接口和更可测试的代码。 + +**原则:** 如果在编写 `fn` 之前能用 `expect(fn(input)).toBe(output)` 描述行为,TDD 会改善结果。 + +**关键洞察:** TDD 工作本质上比标准任务更重 —— 它需要 2-3 个执行周期(RED → GREEN → REFACTOR),每个周期都涉及文件读取、测试运行和可能的调试。TDD 功能获得专门的计划,以确保整个周期内有完整的上下文可用。 + + + +## 何时 TDD 提高质量 + +**TDD 候选(创建 TDD 计划):** +- 有明确输入/输出的业务逻辑 +- 有请求/响应契约的 API 端点 +- 数据转换、解析、格式化 +- 验证规则和约束 +- 有可测试行为的算法 +- 状态机和工作流 +- 有清晰规格的工具函数 + +**跳过 TDD(使用带 `type="auto"` 任务的标准计划):** +- UI 布局、样式、视觉组件 +- 配置更改 +- 连接现有组件的胶水代码 +- 一次性脚本和迁移 +- 无业务逻辑的简单 CRUD +- 探索性原型 + +**启发式:** 能在编写 `fn` 之前写 `expect(fn(input)).toBe(output)` 吗? +→ 能:创建 TDD 计划 +→ 不能:使用标准计划,事后添加测试(如需要) + + + +## TDD 计划结构 + +每个 TDD 计划通过完整的 RED-GREEN-REFACTOR 循环实现**一个功能**。 + +```markdown +--- +phase: XX-name +plan: NN +type: tdd +--- + + +[什么功能以及为什么] +Purpose: [该功能 TDD 的设计收益] +Output: [可工作的、已测试的功能] + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@relevant/source/files.ts + + + + [功能名称] + [源文件, 测试文件] + + [可测试术语描述的预期行为] + Cases: 输入 → 预期输出 + + [测试通过后如何实现] + + + +[证明功能有效的测试命令] + + + +- 失败测试已编写并提交 +- 实现通过测试 +- 重构完成(如需要) +- 所有 2-3 个提交都存在 + + + +完成后,创建包含以下内容的 SUMMARY.md: +- RED: 编写了什么测试,为什么失败 +- GREEN: 什么实现让它通过 +- REFACTOR: 做了什么清理(如有) +- Commits: 生成的提交列表 + +``` + +**每个 TDD 计划一个功能。** 如果功能足够简单可以批量处理,那就足够简单可以跳过 TDD —— 使用标准计划,事后添加测试。 + + + +## 红-绿-重构循环 + +**RED - 编写失败测试:** +1. 按项目约定创建测试文件 +2. 编写描述预期行为的测试(来自 `` 元素) +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 个原子提交。 + + + +## 好测试 vs 坏测试 + +**测试行为,而非实现:** +- 好:"返回格式化的日期字符串" +- 坏:"用正确参数调用 formatDate 辅助函数" +- 测试应该能经受重构 + +**每个测试一个概念:** +- 好:分别为有效输入、空输入、畸形输入编写测试 +- 坏:用多个断言检查所有边缘情况的单个测试 + +**描述性名称:** +- 好:"should reject empty email"、"returns null for invalid ID" +- 坏:"test1"、"handles error"、"works correctly" + +**不包含实现细节:** +- 好:测试公共 API、可观察行为 +- 坏:Mock 内部实现、测试私有方法、断言内部状态 + + + +## 测试框架设置(如不存在) + +当执行 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 阶段的一次性成本。 + + + +## 错误处理 + +**测试在 RED 阶段没有失败:** +- 功能可能已存在 - 调查 +- 测试可能有误(没测试你以为的东西) +- 前进前修复 + +**测试在 GREEN 阶段没有通过:** +- 调试实现 +- 不要跳到重构 +- 持续迭代直到绿色 + +**测试在 REFACTOR 阶段失败:** +- 撤销重构 +- 提交过早 +- 用更小的步骤重构 + +**不相关的测试失败:** +- 停下来调查 +- 可能表明耦合问题 +- 前进前修复 + + + +## 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 纪律的清晰历史 +- 与整体提交策略一致 + + + +## 上下文预算 + +TDD 计划目标 **~40% 上下文使用率**(低于标准计划的 ~50%)。 + +为什么更低: +- RED 阶段:编写测试、运行测试、可能调试为什么没有失败 +- GREEN 阶段:实现、运行测试、可能对失败进行迭代 +- REFACTOR 阶段:修改代码、运行测试、验证无回归 + +每个阶段涉及读取文件、运行命令、分析输出。来回往复本质上比线性任务执行更重。 + +单一功能聚焦确保整个周期保持完整质量。 + \ No newline at end of file diff --git a/docs/zh-CN/references/ui-brand.md b/docs/zh-CN/references/ui-brand.md new file mode 100644 index 000000000..f9828ae1b --- /dev/null +++ b/docs/zh-CN/references/ui-brand.md @@ -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 已写入 +``` + +--- + +## 下一步区块 + +始终在主要完成后。 + +``` +─────────────────────────────────────────────────────────────── + +## ▶ 下一步 + +**{标识符}: {名称}** — {单行描述} + +`{可复制粘贴的命令}` + +`/clear` 优先 → 全新上下文窗口 + +─────────────────────────────────────────────────────────────── + +**也可选:** +- `/gsd:alternative-1` — 描述 +- `/gsd:alternative-2` — 描述 + +─────────────────────────────────────────────────────────────── +``` + +--- + +## 错误框 + +``` +╔══════════════════════════════════════════════════════════════╗ +║ ERROR ║ +╚══════════════════════════════════════════════════════════════╝ + +{错误描述} + +**修复方法:** {解决步骤} +``` + +--- + +## 表格 + +``` +| 阶段 | 状态 | 计划 | 进度 | +|------|------|------|------| +| 1 | ✓ | 3/3 | 100% | +| 2 | ◆ | 1/4 | 25% | +| 3 | ○ | 0/2 | 0% | +``` + +--- + +## 反模式 + +- 变化的框/横幅宽度 +- 混合横幅样式(`===`、`---`、`***`) +- 横幅中缺少 `GSD ►` 前缀 +- 随机 emoji(`🚀`、`✨`、`💫`) +- 完成后缺少下一步区块 \ No newline at end of file diff --git a/docs/zh-CN/references/verification-patterns.md b/docs/zh-CN/references/verification-patterns.md new file mode 100644 index 000000000..cfeeb8dd0 --- /dev/null +++ b/docs/zh-CN/references/verification-patterns.md @@ -0,0 +1,612 @@ +# 验证模式 + +如何验证不同类型的工件是真实实现,而非存根或占位符。 + + +**存在 ≠ 实现** + +文件存在并不意味着功能有效。验证必须检查: +1. **存在** - 文件在预期路径 +2. **实质性** - 内容是真实实现,非占位符 +3. **已连接** - 已连接到系统的其他部分 +4. **功能性** - 调用时实际工作 + +级别 1-3 可以编程检查。级别 4 通常需要人工验证。 + + + + +## 通用存根模式 + +这些模式表明占位符代码,无论文件类型: + +**基于注释的存根:** +```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" # 硬编码显示值 +``` + + + + + +## 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
Component
+return
Placeholder
+return
{/* TODO */}
+return

Coming soon

+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" +``` + +**功能验证(需要人工):** +- 组件是否渲染可见内容? +- 交互元素是否响应点击? +- 数据是否加载并显示? +- 错误状态是否适当显示? + +
+ + + +## 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 是否实际创建记录? +- 错误响应是否有正确的状态码? +- 认证检查是否实际执行? + + + + + +## 数据库模式(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" +``` + + + + + +## 自定义 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" +``` + + + + + +## 环境变量和配置 + +**存在检查:** +```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 +``` + + + + + +## 连接验证模式 + +连接验证检查组件是否实际通信。这是大多数存根隐藏的地方。 + +### 模式:组件 → 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
+

Message 1

+

Message 2

+
+ +// 状态存在但未渲染: +const [messages, setMessages] = useState([]) +return
No messages
// 总是显示 "no messages" + +// 渲染错误的状态: +const [messages, setMessages] = useState([]) +return
{otherData.map(...)}
// 使用不同数据 +``` + +
+ + + +## 快速验证清单 + +对于每种工件类型,运行此清单: + +### 组件清单 +- [ ] 文件存在于预期路径 +- [ ] 导出函数/const 组件 +- [ ] 返回 JSX(非 null/空) +- [ ] 渲染中无占位符文本 +- [ ] 使用 props 或 state(非静态) +- [ ] 事件处理器有真实实现 +- [ ] 导入正确解析 +- [ ] 在应用某处被使用 + +### API 路由清单 +- [ ] 文件存在于预期路径 +- [ ] 导出 HTTP 方法处理器 +- [ ] 处理器超过 5 行 +- [ ] 查询数据库或服务 +- [ ] 返回有意义的响应(非空/占位符) +- [ ] 有错误处理 +- [ ] 验证输入 +- [ ] 从前端调用 + +### 模式清单 +- [ ] 模型/表已定义 +- [ ] 有所有预期字段 +- [ ] 字段有适当类型 +- [ ] 如需要关系已定义 +- [ ] 迁移存在且已应用 +- [ ] 客户端已生成 + +### Hook/工具清单 +- [ ] 文件存在于预期路径 +- [ ] 导出函数 +- [ ] 有有意义的实现(非空返回) +- [ ] 在应用某处被使用 +- [ ] 返回值被消费 + +### 连接清单 +- [ ] 组件 → API: fetch/axios 调用存在且使用响应 +- [ ] API → 数据库: 查询存在且结果返回 +- [ ] 表单 → 处理器: onSubmit 调用 API/mutation +- [ ] 状态 → 渲染: 状态变量出现在 JSX 中 + + + + + +## 自动化验证方法 + +对于验证子代理,使用此模式: + +```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。 + + + + + +## 何时需要人工验证 + +有些事情无法编程验证。标记这些需要人工测试: + +**始终人工:** +- 视觉外观(看起来对吗?) +- 用户流程完成(能实际做那件事吗?) +- 实时行为(WebSocket、SSE) +- 外部服务集成(Stripe、邮件发送) +- 错误消息清晰度(消息有帮助吗?) +- 性能感觉(感觉快吗?) + +**如不确定则人工:** +- grep 无法追踪的复杂连接 +- 依赖状态的动态行为 +- 边缘情况和错误状态 +- 移动端响应式 +- 无障碍性 + +**人工验证请求格式:** +```markdown +## 需要人工验证 + +### 1. 聊天消息发送 +**测试:** 输入消息并点击发送 +**预期:** 消息出现在列表中,输入框清空 +**检查:** 刷新后消息是否持久? + +### 2. 错误处理 +**测试:** 断开网络,尝试发送 +**预期:** 错误消息出现,消息未丢失 +**检查:** 重连后能重试吗? +``` + + + + + +## 检查点前自动化 + +关于自动化优先的检查点模式、服务器生命周期管理、CLI 安装处理和错误恢复协议,请参阅: + +**@~/.claude/get-shit-done/references/checkpoints.md** → `` 部分 + +关键原则: +- Claude 在呈现检查点**之前**设置验证环境 +- 用户从不运行 CLI 命令(仅访问 URL) +- 服务器生命周期:检查点前启动、处理端口冲突、持续运行 +- CLI 安装:安全处自动安装,否则检查点让用户选择 +- 错误处理:检查点前修复损坏环境,绝不呈现有失败设置的检查点 + + \ No newline at end of file diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index f0246b741..f2b455397 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -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 Get model for agent based on profile * find-phase Find phase directory by number - * commit [--files f1 f2] Commit planning docs + * commit [--files f1 f2] [--no-verify] Commit planning docs + * commit-to-subrepo --files f1 f2 Route commits to sub-repos * verify-summary Verify a SUMMARY.md file * generate-slug Convert text to URL-safe slug * current-timestamp [format] Get timestamp (full|date|filename) @@ -33,7 +36,7 @@ * * Phase Operations: * phase next-decimal Calculate next decimal phase number - * phase add Append new phase to roadmap + create dir + * phase add [--id ID] Append new phase to roadmap + create dir * phase insert Insert decimal phase after existing * phase remove [--force] Remove phase, renumber all subsequent * phase complete Mark phase done, update state + roadmap @@ -62,6 +65,9 @@ * Todos: * todo complete Move todo from pending to completed * + * UAT Audit: + * audit-uat Scan all phases for unresolved UAT/verification items + * * Scaffolding: * scaffold context --phase Create CONTEXT.md template * scaffold uat --phase 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 [args] [--raw] [--cwd ]\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; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 73decc8bd..f0d95a3a0 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -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, }; diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 0625d80dc..983b493e3 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -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, diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..edcf95e3c 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -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/ + // 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(/
[\s\S]*?<\/details>/gi, ''); } +/** + * Extract the current milestone section from ROADMAP.md by positive lookup. + * + * Instead of stripping
blocks (negative heuristic that breaks if + * agents wrap the current milestone in
), 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
blocks in it (these are definitely shipped) + const preamble = beforeMilestones.replace(/
[\s\S]*?<\/details>/gi, ''); + + return preamble + currentSection; +} + /** * Replace a pattern only in the current milestone section of ROADMAP.md * (everything after the last
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
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, }; diff --git a/get-shit-done/bin/lib/frontmatter.cjs b/get-shit-done/bin/lib/frontmatter.cjs index e5f500a68..d7bb698dd 100644 --- a/get-shit-done/bin/lib/frontmatter.cjs +++ b/get-shit-done/bin/lib/frontmatter.cjs @@ -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]; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..6083dd908 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -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 = { diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 6fd032798..a86584d2f 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -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 ]/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 = { diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d88be9459..5b01f2bbd 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -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); diff --git a/get-shit-done/bin/lib/profile-output.cjs b/get-shit-done/bin/lib/profile-output.cjs index ec6264f52..9d2add389 100644 --- a/get-shit-done/bin/lib/profile-output.cjs +++ b/get-shit-done/bin/lib/profile-output.cjs @@ -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 = [ '', '## 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 = {}; diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 3164b702c..693baf4f5 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -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({ diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..a01aafd66 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -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, }; diff --git a/get-shit-done/bin/lib/uat.cjs b/get-shit-done/bin/lib/uat.cjs new file mode 100644 index 000000000..652fae16e --- /dev/null +++ b/get-shit-done/bin/lib/uat.cjs @@ -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 }; diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 9f8a08546..57adb1878 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -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, diff --git a/get-shit-done/references/checkpoints.md b/get-shit-done/references/checkpoints.md index 232817480..23f90e99c 100644 --- a/get-shit-done/references/checkpoints.md +++ b/get-shit-done/references/checkpoints.md @@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Start dev server for verification Run `npm run dev` in background, wait for "ready" message, capture port - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Deploy to Vercel .vercel/, vercel.json Run `vercel --yes` to deploy - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 @@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Retry Vercel deployment Run `vercel --yes` (now authenticated) - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 ``` @@ -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 @@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Fix server startup issue Investigate error, fix root cause, restart server - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server for auth testing Run `npm run dev` in background, wait for ready signal - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server Run `npm run dev` in background - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Deploy to Vercel Run `vercel --yes`. Capture URL. - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 diff --git a/get-shit-done/references/git-integration.md b/get-shit-done/references/git-integration.md index 1e0a9d1b4..ef530533d 100644 --- a/get-shit-done/references/git-integration.md +++ b/get-shit-done/references/git-integration.md @@ -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 + + + +## 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. + + diff --git a/get-shit-done/references/model-profiles.md b/get-shit-done/references/model-profiles.md index e4d5f0649..97a92b75d 100644 --- a/get-shit-done/references/model-profiles.md +++ b/get-shit-done/references/model-profiles.md @@ -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: diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md index 3bb884377..f8276c761 100644 --- a/get-shit-done/references/planning-config.md +++ b/get-shit-done/references/planning-config.md @@ -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 | diff --git a/get-shit-done/templates/UAT.md b/get-shit-done/templates/UAT.md index 5e2118ed2..fd2345a98 100644 --- a/get-shit-done/templates/UAT.md +++ b/get-shit-done/templates/UAT.md @@ -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] **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 diff --git a/get-shit-done/templates/claude-md.md b/get-shit-done/templates/claude-md.md index f19ec3f01..240146a28 100644 --- a/get-shit-done/templates/claude-md.md +++ b/get-shit-done/templates/claude-md.md @@ -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 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. + +``` + ### Profile Section (Placeholder Only) ``` @@ -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 diff --git a/get-shit-done/templates/config.json b/get-shit-done/templates/config.json index 462e63628..6b8b46064 100644 --- a/get-shit-done/templates/config.json +++ b/get-shit-done/templates/config.json @@ -10,7 +10,8 @@ }, "planning": { "commit_docs": true, - "search_gitignored": false + "search_gitignored": false, + "sub_repos": [] }, "parallelization": { "enabled": true, diff --git a/get-shit-done/templates/discussion-log.md b/get-shit-done/templates/discussion-log.md new file mode 100644 index 000000000..37cd86692 --- /dev/null +++ b/get-shit-done/templates/discussion-log.md @@ -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 `` 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 diff --git a/get-shit-done/templates/phase-prompt.md b/get-shit-done/templates/phase-prompt.md index 6d23160dd..b242dc15e 100644 --- a/get-shit-done/templates/phase-prompt.md +++ b/get-shit-done/templates/phase-prompt.md @@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components. Task 2: Create User API endpoints src/features/user/api.ts GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model. - curl tests pass for all endpoints + fetch tests pass for all endpoints All CRUD operations work @@ -407,7 +407,7 @@ Output: Working dashboard component. Start dev server Run `npm run dev` in background, wait for ready - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 diff --git a/get-shit-done/templates/project.md b/get-shit-done/templates/project.md index 8971f4528..37a986c70 100644 --- a/get-shit-done/templates/project.md +++ b/get-shit-done/templates/project.md @@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform 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 diff --git a/get-shit-done/workflows/audit-uat.md b/get-shit-done/workflows/audit-uat.md new file mode 100644 index 000000000..13e95ce79 --- /dev/null +++ b/get-shit-done/workflows/audit-uat.md @@ -0,0 +1,109 @@ + +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. + + + + + +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. + + + +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` + + + +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}` +``` + + + +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} +... +``` + + + diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index d8f7a6f40..be37d214f 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -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. @@ -242,6 +254,47 @@ Structure the extracted information: **If no prior context exists:** Continue without — this is expected for early phases. + +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 `` for inclusion in CONTEXT.md `` section +- These become additional scope items that downstream agents (researcher, planner) will see + +**For unselected (reviewed but not folded) todos:** +- Store internally as `` for inclusion in CONTEXT.md `` 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. + + 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. 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. 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.] + @@ -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"] @@ -648,10 +774,54 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md -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" diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index af9027722..67a6e3583 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -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. + +**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. + + Read STATE.md before any operation to load project context. + +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 + + @@ -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 ``). 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 ``` + +**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). + + 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. + + 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 "..." + + @~/.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` 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) + + 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. + + - [ ] 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 ``` + +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. + + 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 ``` + +**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. + + **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. -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 diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index d47b6a18f..437e8302a 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -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. @@ -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 `` 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 @@ -273,6 +276,20 @@ git add src/types/user.ts **4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes. + +**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. + + **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". diff --git a/get-shit-done/workflows/fast.md b/get-shit-done/workflows/fast.md new file mode 100644 index 000000000..729bcc32e --- /dev/null +++ b/get-shit-done/workflows/fast.md @@ -0,0 +1,105 @@ + +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. + + + + + +Parse `$ARGUMENTS` for the task description. + +If empty, ask: +``` +What's the quick fix? (one sentence) +``` + +Store as `$TASK`. + + + +**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. + + + +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. + + + +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. + + + +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 +``` + + + +Report completion: + +``` +✅ Done: {what was changed} + Commit: {short hash} + Files: {list of changed files} +``` + +No next-step suggestions. No workflow routing. Just done. + + + + + +- 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 + + + +- [ ] 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 + diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md index 54b7a1423..c8c6b4eeb 100644 --- a/get-shit-done/workflows/health.md +++ b/get-shit-done/workflows/health.md @@ -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 + + +**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)` + diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 058d4a810..7fb488b8b 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -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 `** @@ -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 `** 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]`** diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index 901b14511..28725aa9b 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -82,12 +82,25 @@ mkdir -p .planning/codebase Continue to spawn_agents. - + +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. + + + 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. -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. + +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. + + Verify all documents created successfully: @@ -307,10 +361,10 @@ End workflow. - .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 diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 59b894bbb..a2b1e3fd0 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -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=" 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]` diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 893de103e..3d93f6cc9 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -7,11 +7,13 @@ Read all files referenced by the invoking prompt's execution_context before star + ## 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. ``` + @@ -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=" @@ -983,15 +1076,24 @@ Use AskUserQuestion: ", 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` @@ -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. diff --git a/get-shit-done/workflows/next.md b/get-shit-done/workflows/next.md new file mode 100644 index 000000000..80e2f3622 --- /dev/null +++ b/get-shit-done/workflows/next.md @@ -0,0 +1,97 @@ + +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. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +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. + + + +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 ` + +**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 ` + +**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 ` + +**Route 4: Phase has plans but incomplete summaries → execute** +If plans exist but not all have matching summaries: +→ Next action: `/gsd:execute-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 ` + +**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` + + + +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. + + + + + +- [ ] Project state correctly detected +- [ ] Next action correctly determined from routing rules +- [ ] Command invoked immediately without user confirmation +- [ ] Clear status shown before invoking + diff --git a/get-shit-done/workflows/pause-work.md b/get-shit-done/workflows/pause-work.md index f723ef81a..ccdba267d 100644 --- a/get-shit-done/workflows/pause-work.md +++ b/get-shit-done/workflows/pause-work.md @@ -1,5 +1,5 @@ -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. @@ -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. + + + +**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}" +} +``` @@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta ```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 ``` ``` -✓ 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 diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index f7114c5a8..26697dc3e 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -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 + +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 + + ## 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 `` section. Extract feature/capability names. Check each against plan `` 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 `` 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} ─────────────────────────────────────────────────────────────── + +**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 +``` + + - [ ] .planning/ directory validated - [ ] Phase validated against roadmap diff --git a/get-shit-done/workflows/plant-seed.md b/get-shit-done/workflows/plant-seed.md new file mode 100644 index 000000000..918667cef --- /dev/null +++ b/get-shit-done/workflows/plant-seed.md @@ -0,0 +1,169 @@ + +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 + + + + + +Parse `$ARGUMENTS` for the idea summary. + +If empty, ask: +``` +What's the idea? (one sentence) +``` + +Store as `$IDEA`. + + + +```bash +mkdir -p .planning/seeds +``` + + + +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`. + + + +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`. + + + +```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. + + + +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} +``` + + + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md +``` + + + +``` +✅ 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. +``` + + + + + +- [ ] Seed file created in .planning/seeds/ +- [ ] Frontmatter includes status, trigger, scope +- [ ] Breadcrumbs collected from codebase +- [ ] Committed to git +- [ ] User shown confirmation with trigger info + diff --git a/get-shit-done/workflows/pr-branch.md b/get-shit-done/workflows/pr-branch.md new file mode 100644 index 000000000..11697d473 --- /dev/null +++ b/get-shit-done/workflows/pr-branch.md @@ -0,0 +1,129 @@ + +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. + + + + + +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 +``` + + + +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) +``` + + + +```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" +``` + + + +```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. +``` + + + + + +- [ ] 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 + diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index 83bf5825b..c0ba54a65 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -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 + +`/clear` first → fresh context window + +--- + +**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: diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index 4f21ebad0..7138dd031 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -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 diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 00ce54df0..a8dafcf2c 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -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 diff --git a/get-shit-done/workflows/review.md b/get-shit-done/workflows/review.md new file mode 100644 index 000000000..99a13da95 --- /dev/null +++ b/get-shit-done/workflows/review.md @@ -0,0 +1,228 @@ + +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. + + + + + +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. + + + +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) + + + +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` + + + +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 ✓ +``` + + + +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 +``` + + + +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. + + + + + +- [ ] 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) + diff --git a/get-shit-done/workflows/session-report.md b/get-shit-done/workflows/session-report.md new file mode 100644 index 000000000..f336edc08 --- /dev/null +++ b/get-shit-done/workflows/session-report.md @@ -0,0 +1,146 @@ + +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. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +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" +``` + + + +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 + + + +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`* +``` + + + +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. +``` + + + + + +- [ ] 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 + diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 7fc344559..5bf50e48c 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -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": }, "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": , "parallelization": , "branching_strategy": , + "quick_branch_template": , "workflow": { "research": , "plan_check": , diff --git a/get-shit-done/workflows/ship.md b/get-shit-done/workflows/ship.md new file mode 100644 index 000000000..3c29de1ff --- /dev/null +++ b/get-shit-done/workflows/ship.md @@ -0,0 +1,228 @@ + +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. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +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`. + + + +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. + + + +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)" + + + +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} +``` + + + +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}" + + + +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" + + + +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 +``` + + + +``` +─────────────────────────────────────────────────────────────── + +## ✓ 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) + +─────────────────────────────────────────────────────────────── +``` + + + + + +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 + + + +- [ ] 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 + diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 5e8927dfd..dec8af17a 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -1,3 +1,19 @@ + + +**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 + + + **Read these files NOW:** @@ -58,6 +74,30 @@ cat .planning/config.json 2>/dev/null +**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:** diff --git a/get-shit-done/workflows/update.md b/get-shit-done/workflows/update.md index 7d276eae4..fa910c927 100644 --- a/get-shit-done/workflows/update.md +++ b/get-shit-done/workflows/update.md @@ -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_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex" +# Runtime candidates: ":" 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 diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index deed81d19..7cade7f32 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -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`. **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: diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js index b9a6075ed..9076ec038 100755 --- a/hooks/gsd-check-update.js +++ b/hooks/gsd-check-update.js @@ -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)); diff --git a/hooks/gsd-context-monitor.js b/hooks/gsd-context-monitor.js index d7a5eff06..ae1bbf9a3 100644 --- a/hooks/gsd-context-monitor.js +++ b/hooks/gsd-context-monitor.js @@ -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', () => { diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index d88ca4a2c..ae7025b99 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -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) {} } diff --git a/hooks/gsd-workflow-guard.js b/hooks/gsd-workflow-guard.js new file mode 100644 index 000000000..d8075aaf6 --- /dev/null +++ b/hooks/gsd-workflow-guard.js @@ -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); + } +}); diff --git a/package-lock.json b/package-lock.json index 3ffa43783..2e1af42c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index a03f6c366..5d31df501 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index ffb60b0ff..b1b8fa416 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -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.'); diff --git a/tests/agent-frontmatter.test.cjs b/tests/agent-frontmatter.test.cjs index a5aea5264..a5b4f6412 100644 --- a/tests/agent-frontmatter.test.cjs +++ b/tests/agent-frontmatter.test.cjs @@ -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' + ); + }); +}); diff --git a/tests/claude-md.test.cjs b/tests/claude-md.test.cjs new file mode 100644 index 000000000..3443f7783 --- /dev/null +++ b/tests/claude-md.test.cjs @@ -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`')); + }); +}); diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 4c2cd0aff..6f7587d29 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -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 = `You are an unknown agent.`; + 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'); + }); +}); diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index b43d2827a..4e4c74216 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -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 // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/config.test.cjs b/tests/config.test.cjs index 65b62aa4f..0ec5659ec 100644 --- a/tests/config.test.cjs +++ b/tests/config.test.cjs @@ -222,6 +222,16 @@ describe('config-set command', () => { ); }); + test('sets workflow.text_mode for remote session support', () => { + writeConfig(tmpDir, {}); + + const result = runGsdTools('config-set workflow.text_mode true', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const config = readConfig(tmpDir); + assert.strictEqual(config.workflow.text_mode, true); + }); + test('errors when no key path provided', () => { const result = runGsdTools('config-set', tmpDir); assert.strictEqual(result.success, false); diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 158ea974e..ee162d4c4 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 39, `expected 39 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 50, `expected 50 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 39; +const EXPECTED_SKILLS = 50; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { diff --git a/tests/core.test.cjs b/tests/core.test.cjs index b20328f32..77fb46956 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -10,6 +10,7 @@ const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const { createTempProject, cleanup } = require('./helpers.cjs'); const { loadConfig, @@ -26,6 +27,8 @@ const { getRoadmapPhaseInternal, searchPhaseInDir, findPhaseInternal, + findProjectRoot, + detectSubRepos, } = require('../get-shit-done/bin/lib/core.cjs'); // ─── loadConfig ──────────────────────────────────────────────────────────────── @@ -35,14 +38,13 @@ describe('loadConfig', () => { let originalCwd; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); originalCwd = process.cwd(); }); afterEach(() => { process.chdir(originalCwd); - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -61,6 +63,7 @@ describe('loadConfig', () => { assert.strictEqual(config.brave_search, false); assert.strictEqual(config.parallelization, true); assert.strictEqual(config.nyquist_validation, true); + assert.strictEqual(config.text_mode, false); }); test('reads model_profile from config.json', () => { @@ -129,12 +132,11 @@ describe('resolveModelInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -276,62 +278,11 @@ describe('generateSlugInternal', () => { }); }); -// ─── normalizePhaseName ──────────────────────────────────────────────────────── - -describe('normalizePhaseName', () => { - test('pads single digit', () => { - assert.strictEqual(normalizePhaseName('1'), '01'); - }); - - test('preserves double digit', () => { - assert.strictEqual(normalizePhaseName('12'), '12'); - }); - - test('handles letter suffix', () => { - assert.strictEqual(normalizePhaseName('1A'), '01A'); - }); - - test('handles decimal phases', () => { - assert.strictEqual(normalizePhaseName('2.1'), '02.1'); - }); - - test('handles multi-level decimals', () => { - assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3'); - }); - - test('returns non-matching input unchanged', () => { - assert.strictEqual(normalizePhaseName('abc'), 'abc'); - }); -}); - -// ─── comparePhaseNum ─────────────────────────────────────────────────────────── - -describe('comparePhaseNum', () => { - test('sorts integer phases numerically', () => { - assert.ok(comparePhaseNum('1', '2') < 0); - assert.ok(comparePhaseNum('10', '2') > 0); - }); - - test('sorts letter suffixes', () => { - assert.ok(comparePhaseNum('12', '12A') < 0); - assert.ok(comparePhaseNum('12A', '12B') < 0); - }); - - test('sorts decimal phases', () => { - assert.ok(comparePhaseNum('2', '2.1') < 0); - assert.ok(comparePhaseNum('2.1', '2.2') < 0); - }); - - test('handles multi-level decimals', () => { - assert.ok(comparePhaseNum('1.1', '1.1.2') < 0); - assert.ok(comparePhaseNum('1.1.2', '1.2') < 0); - }); - - test('returns 0 for equal phases', () => { - assert.strictEqual(comparePhaseNum('1', '1'), 0); - assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0); - }); -}); +// ─── normalizePhaseName / comparePhaseNum ────────────────────────────────────── +// NOTE: Comprehensive tests for normalizePhaseName and comparePhaseNum are in +// phase.test.cjs (which covers all edge cases: hybrid, letter-suffix, +// multi-level decimal, case-insensitive, directory-slug, and full sort order). +// Removed duplicates here to keep a single authoritative test location. // ─── safeReadFile ────────────────────────────────────────────────────────────── @@ -343,7 +294,7 @@ describe('safeReadFile', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('reads existing file', () => { @@ -363,12 +314,11 @@ describe('pathExistsInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('returns true for existing path', () => { @@ -390,12 +340,11 @@ describe('getMilestoneInfo', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('extracts version and name from roadmap', () => { @@ -502,7 +451,7 @@ describe('searchPhaseInDir', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase directory by normalized prefix', () => { @@ -564,12 +513,11 @@ describe('findPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase in current phases directory', () => { @@ -605,12 +553,11 @@ describe('getRoadmapPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); // Bug: getRoadmapPhaseInternal was missing from module.exports @@ -687,12 +634,11 @@ describe('getMilestonePhaseFilter', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('filters directories to only current milestone phases', () => { @@ -924,3 +870,382 @@ describe('normalizeMd', () => { assert.ok(result.includes('\n\n- Decision 1'), 'list needs blank line before'); }); }); + +// ─── Stale hook filter regression (#1200) ───────────────────────────────────── + +describe('stale hook filter', () => { + test('filter should only match gsd-prefixed .js files', () => { + const files = [ + 'gsd-check-update.js', + 'gsd-context-monitor.js', + 'gsd-statusline.js', + 'gsd-workflow-guard.js', + 'guard-edits-outside-project.js', // user hook + 'my-custom-hook.js', // user hook + 'gsd-check-update.js.bak', // backup file + 'README.md', // non-js file + ]; + + const gsdFilter = f => f.startsWith('gsd-') && f.endsWith('.js'); + const filtered = files.filter(gsdFilter); + + assert.deepStrictEqual(filtered, [ + 'gsd-check-update.js', + 'gsd-context-monitor.js', + 'gsd-statusline.js', + 'gsd-workflow-guard.js', + ], 'should only include gsd-prefixed .js files'); + + assert.ok(!filtered.includes('guard-edits-outside-project.js'), 'must not include user hooks'); + assert.ok(!filtered.includes('my-custom-hook.js'), 'must not include non-gsd hooks'); + }); +}); + +// ─── resolveWorktreeRoot ───────────────────────────────────────────────────── + +describe('resolveWorktreeRoot', () => { + const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs'); + + test('returns cwd when not in a git repo', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-')); + try { + assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('returns cwd in a normal git repo (not a worktree)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-')); + try { + const { execSync } = require('child_process'); + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); + assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// ─── withPlanningLock ──────────────────────────────────────────────────────── + +describe('withPlanningLock', () => { + const { withPlanningLock, planningDir } = require('../get-shit-done/bin/lib/core.cjs'); + + test('executes function and returns result', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + try { + const result = withPlanningLock(tmpDir, () => 42); + assert.strictEqual(result, 42); + // Lock file should be cleaned up + assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock'))); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('cleans up lock file even on error', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + try { + assert.throws(() => { + withPlanningLock(tmpDir, () => { throw new Error('test'); }); + }, /test/); + assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock'))); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('recovers from stale lock (>30s old)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + const planDir = path.join(tmpDir, '.planning'); + fs.mkdirSync(planDir, { recursive: true }); + const lockPath = path.join(planDir, '.lock'); + try { + // Create a stale lock + fs.writeFileSync(lockPath, '{"pid":99999}'); + // Backdate the lock file by 31 seconds + const staleTime = new Date(Date.now() - 31000); + fs.utimesSync(lockPath, staleTime, staleTime); + + const result = withPlanningLock(tmpDir, () => 'recovered'); + assert.strictEqual(result, 'recovered'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// ─── detectSubRepos ────────────────────────────────────────────────────────── + +describe('detectSubRepos', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-detect-test-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('returns empty array when no child directories have .git', () => { + fs.mkdirSync(path.join(projectRoot, 'src')); + fs.mkdirSync(path.join(projectRoot, 'lib')); + assert.deepStrictEqual(detectSubRepos(projectRoot), []); + }); + + test('detects directories with .git', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'scripts')); // no .git + assert.deepStrictEqual(detectSubRepos(projectRoot), ['backend', 'frontend']); + }); + + test('returns sorted results', () => { + fs.mkdirSync(path.join(projectRoot, 'zeta', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'alpha', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'mid', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['alpha', 'mid', 'zeta']); + }); + + test('skips hidden directories', () => { + fs.mkdirSync(path.join(projectRoot, '.hidden', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'visible', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['visible']); + }); + + test('skips node_modules', () => { + fs.mkdirSync(path.join(projectRoot, 'node_modules', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'app', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['app']); + }); +}); + +// ─── loadConfig sub_repos auto-sync ────────────────────────────────────────── + +describe('loadConfig sub_repos auto-sync', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-sync-test-')); + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('migrates multiRepo: true to sub_repos array', () => { + // Create config with legacy multiRepo flag + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true, model_profile: 'quality' }) + ); + // Create sub-repos + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend', 'frontend']); + assert.strictEqual(config.commit_docs, false); + + // Verify config was persisted + const saved = JSON.parse(fs.readFileSync(path.join(projectRoot, '.planning', 'config.json'), 'utf-8')); + assert.deepStrictEqual(saved.sub_repos, ['backend', 'frontend']); + assert.strictEqual(saved.multiRepo, undefined, 'multiRepo should be removed'); + }); + + test('adds newly detected repos to sub_repos', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend'] }) + ); + + // Add a new repo + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend', 'frontend']); + }); + + test('removes repos that no longer have .git', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'old-repo'] }) + ); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend']); + }); + + test('does not sync when sub_repos is empty and no repos detected', () => { + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: [] }) + ); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, []); + }); +}); + +// ─── findProjectRoot ───────────────────────────────────────────────────────── + +describe('findProjectRoot', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-root-test-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('returns startDir when no .planning/ exists anywhere', () => { + const subDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(subDir); + assert.strictEqual(findProjectRoot(subDir), subDir); + }); + + test('returns startDir when .planning/ is in startDir itself', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + assert.strictEqual(findProjectRoot(projectRoot), projectRoot); + }); + + test('walks up to parent with .planning/ and sub_repos config listing this dir', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('walks up from nested sub-repo subdirectory', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + + const deepDir = path.join(projectRoot, 'backend', 'src', 'services'); + fs.mkdirSync(deepDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(deepDir), projectRoot); + }); + + test('walks up via legacy multiRepo flag', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('walks up via .git heuristic when no config exists', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + // No config.json at all + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('walks up from nested path inside sub-repo via .git heuristic', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + // Sub-repo with .git at its root + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(backendDir, 'src', 'modules', 'auth'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // isInsideGitRepo walks up and finds backend/.git + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path inside sub-repo via sub_repos config', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend'] }) + ); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(projectRoot, 'backend', 'src', 'modules'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // With sub_repos config, it checks topSegment of relative path + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path via legacy multiRepo flag', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested inside sub-repo — isInsideGitRepo walks up and finds backend/.git + const nestedDir = path.join(backendDir, 'src'); + fs.mkdirSync(nestedDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('does not walk up for dirs without .git when no sub_repos config', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + const scriptsDir = path.join(projectRoot, 'scripts'); + fs.mkdirSync(scriptsDir); + + assert.strictEqual(findProjectRoot(scriptsDir), scriptsDir); + }); + + test('handles planning.sub_repos nested config format', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ planning: { sub_repos: ['backend'] } }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('returns startDir when sub_repos is empty and no .git', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: [] }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), backendDir); + }); +}); diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs new file mode 100644 index 000000000..a6fe43844 --- /dev/null +++ b/tests/cursor-conversion.test.cjs @@ -0,0 +1,81 @@ +/** + * Cursor conversion regression tests. + * + * Ensures Cursor frontmatter names are emitted as plain identifiers + * (without surrounding quotes), so Cursor does not treat quotes as + * literal parts of skill/subagent names. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +const { + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, +} = require('../bin/install.js'); + +describe('convertClaudeCommandToCursorSkill', () => { + test('writes unquoted Cursor skill name in frontmatter', () => { + const input = `--- +name: quick +description: Execute a quick task +--- + + +Test body + +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick'); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.strictEqual(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); + assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); + }); + + test('preserves slash for slash commands in markdown body', () => { + const input = `--- +name: gsd:plan-phase +description: Plan a phase +--- + +Next: +/gsd:execute-phase 17 +/gsd-help +gsd:progress +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-plan-phase'); + + assert.ok(result.includes('/gsd-execute-phase 17'), 'slash command remains slash-prefixed'); + assert.ok(result.includes('/gsd-help'), 'existing slash command is preserved'); + assert.ok(result.includes('gsd-progress'), 'non-slash gsd: references still normalize'); + assert.ok(!result.includes('/gsd:execute-phase'), 'legacy colon command form is removed'); + }); +}); + +describe('convertClaudeAgentToCursorAgent', () => { + test('writes unquoted Cursor agent name in frontmatter', () => { + const input = `--- +name: gsd-planner +description: Planner agent +tools: Read, Write +color: green +--- + + +Planner body + +`; + + const result = convertClaudeAgentToCursorAgent(input); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.strictEqual(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); + assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted'); + }); +}); diff --git a/tests/gemini-config.test.cjs b/tests/gemini-config.test.cjs deleted file mode 100644 index 794208427..000000000 --- a/tests/gemini-config.test.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** - * GSD Tools Tests - Gemini agent conversion - * - * Verifies Gemini-specific agent frontmatter conversion removes - * unsupported fields while preserving converted tools and body text. - */ - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe } = require('node:test'); -const assert = require('node:assert'); - -const { convertClaudeToGeminiAgent } = require('../bin/install.js'); - -describe('convertClaudeToGeminiAgent', () => { - test('drops unsupported skills frontmatter while keeping converted tools', () => { - const input = `--- -name: gsd-codebase-mapper -description: Explores codebase and writes structured analysis documents. -tools: Read, Bash, Grep, Glob, Write -color: cyan -skills: - - gsd-mapper-workflow ---- - - -Use \${PHASE} in shell examples. -`; - - const result = convertClaudeToGeminiAgent(input); - const frontmatter = result.split('---')[1] || ''; - - assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); - assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); - assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); - assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); - assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); - assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); - assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); - assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); - assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); - assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); - assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); - assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); - assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); - }); -}); diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 66a645d7d..740fca823 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -679,6 +679,7 @@ describe('cmdInitQuick', () => { assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); + assert.strictEqual(output.branch_name, null); assert.strictEqual(output.slug, 'fix-login-bug'); assert.strictEqual(output.description, 'Fix login bug'); @@ -736,6 +737,44 @@ describe('cmdInitQuick', () => { const output = JSON.parse(result.output); assert.ok(output.slug.length <= 40, `Slug should be <= 40 chars, got ${output.slug.length}: "${output.slug}"`); }); + + test('returns quick branch name when quick_branch_template is configured', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + git: { + quick_branch_template: 'gsd/quick-{num}-{slug}', + }, + }, null, 2) + ); + + const result = runGsdTools('init quick "Fix login bug"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.branch_name, 'branch_name should be set'); + assert.ok(output.branch_name.startsWith('gsd/quick-')); + assert.ok(output.branch_name.endsWith('-fix-login-bug')); + assert.ok(output.branch_name.includes(output.quick_id), 'branch_name should include quick_id'); + }); + + test('uses fallback slug in quick branch name when description is omitted', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + git: { + quick_branch_template: 'gsd/quick-{quick}-{slug}', + }, + }, null, 2) + ); + + const result = runGsdTools('init quick', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.branch_name, 'branch_name should be set'); + assert.ok(output.branch_name.endsWith('-quick'), `Expected fallback slug in branch name, got "${output.branch_name}"`); + }); }); // ───────────────────────────────────────────────────────────────────────────── @@ -907,6 +946,99 @@ describe('cmdInitNewMilestone', () => { assert.strictEqual(output2.roadmap_exists, true); assert.strictEqual(output2.project_exists, true); }); + + test('reports latest completed milestone and archive target for reset flow', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'MILESTONES.md'), + '# Milestones\n\n## v1.2 Search Refresh (Shipped: 2026-02-18)\n\n---\n' + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-refine-search'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-polish'), { recursive: true }); + + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, 'v1.2'); + assert.strictEqual(output.latest_completed_milestone_name, 'Search Refresh'); + assert.strictEqual(output.phase_dir_count, 2); + assert.strictEqual(output.phase_archive_path, '.planning/milestones/v1.2-phases'); + }); + + test('reset flow metadata is null-safe when no milestones file exists', () => { + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, null); + assert.strictEqual(output.latest_completed_milestone_name, null); + assert.strictEqual(output.phase_dir_count, 0); + assert.strictEqual(output.phase_archive_path, null); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// findProjectRoot integration — gsd-tools resolves project root from sub-repo +// ───────────────────────────────────────────────────────────────────────────── + +describe('findProjectRoot integration via --cwd', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = createTempProject(); + // Add ROADMAP.md so init quick doesn't error + fs.writeFileSync( + path.join(projectRoot, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n## Phase 1: Foundation\n**Goal:** Setup\n' + ); + // Write sub_repos config + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + // Create sub-repo directory + fs.mkdirSync(path.join(projectRoot, 'backend')); + }); + + afterEach(() => { + cleanup(projectRoot); + }); + + test('init quick from sub-repo CWD returns project_root pointing to parent', () => { + const backendDir = path.join(projectRoot, 'backend'); + const result = runGsdTools(['init', 'quick', 'test task', '--cwd', backendDir]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok('project_root' in output, 'Should have project_root'); + assert.strictEqual(output.project_root, projectRoot, 'project_root should be the parent, not the sub-repo'); + assert.ok(output.roadmap_exists, 'Should find ROADMAP.md at project root'); + }); + + test('init quick from project root returns project_root as-is', () => { + const result = runGsdTools(['init', 'quick', 'test task', '--cwd', projectRoot]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_root, projectRoot); + }); + + test('state load from sub-repo CWD reads project root config', () => { + // Write STATE.md at project root + fs.writeFileSync( + path.join(projectRoot, '.planning', 'STATE.md'), + '---\ncurrent_phase: 1\nphase_name: Foundation\n---\n# State\n' + ); + + const backendDir = path.join(projectRoot, 'backend'); + const result = runGsdTools(['state', '--cwd', backendDir]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Should find config from project root, not from backend/ + assert.deepStrictEqual(output.config.sub_repos, ['backend', 'frontend'], + 'Should read sub_repos from project root config'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index fc00a37cd..c3319d10c 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -424,6 +424,75 @@ describe('milestone complete command', () => { assert.strictEqual(output.phases, 2, 'should count only phases 456 and 457'); }); + test('counts tasks from **Tasks:** N in summary body', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync( + path.join(p1, '01-01-SUMMARY.md'), + `---\none-liner: Built the foundation\n---\n\n# Phase 1: Foundation Summary\n\n**Built the foundation**\n\n## Performance\n\n- **Duration:** 28 min\n- **Tasks:** 7\n- **Files modified:** 12\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.tasks, 7, 'should count tasks from **Tasks:** N field'); + }); + + test('extracts one-liner from body when not in frontmatter', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + // No one-liner in frontmatter, but present in body as bold line + fs.writeFileSync( + path.join(p1, '01-01-SUMMARY.md'), + `---\nphase: "01"\n---\n\n# Phase 1: Foundation Summary\n\n**JWT auth with refresh rotation using jose library**\n\n## Performance\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok( + output.accomplishments.includes('JWT auth with refresh rotation using jose library'), + 'should extract one-liner from body bold line' + ); + }); + + test('updates STATE.md with plain format fields', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\nStatus: In progress\nLast Activity: 2025-01-01\nLast Activity Description: Working\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('v1.0 milestone complete'), 'plain Status field should be updated'); + }); + test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), diff --git a/tests/model-profiles.test.cjs b/tests/model-profiles.test.cjs new file mode 100644 index 000000000..55fd1cf00 --- /dev/null +++ b/tests/model-profiles.test.cjs @@ -0,0 +1,134 @@ +/** + * Model Profiles Tests + * + * Tests for MODEL_PROFILES data structure, VALID_PROFILES list, + * formatAgentToModelMapAsTable, and getAgentToModelMapForProfile. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); + +const { + MODEL_PROFILES, + VALID_PROFILES, + formatAgentToModelMapAsTable, + getAgentToModelMapForProfile, +} = require('../get-shit-done/bin/lib/model-profiles.cjs'); + +// ─── MODEL_PROFILES data integrity ──────────────────────────────────────────── + +describe('MODEL_PROFILES', () => { + test('contains all expected GSD agents', () => { + const expectedAgents = [ + 'gsd-planner', 'gsd-roadmapper', 'gsd-executor', + 'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer', + 'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier', + 'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor', + 'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor', + ]; + for (const agent of expectedAgents) { + assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`); + } + }); + + test('every agent has quality, balanced, and budget profiles', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.ok(profiles.quality, `${agent} missing quality profile`); + assert.ok(profiles.balanced, `${agent} missing balanced profile`); + assert.ok(profiles.budget, `${agent} missing budget profile`); + } + }); + + test('all profile values are valid model aliases', () => { + const validModels = ['opus', 'sonnet', 'haiku']; + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + for (const [profile, model] of Object.entries(profiles)) { + assert.ok( + validModels.includes(model), + `${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}` + ); + } + } + }); + + test('quality profile never uses haiku', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.notStrictEqual( + profiles.quality, 'haiku', + `${agent} quality profile should not use haiku` + ); + } + }); +}); + +// ─── VALID_PROFILES ─────────────────────────────────────────────────────────── + +describe('VALID_PROFILES', () => { + test('contains quality, balanced, and budget', () => { + assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']); + }); + + test('is derived from MODEL_PROFILES keys', () => { + const fromData = Object.keys(MODEL_PROFILES['gsd-planner']); + assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort()); + }); +}); + +// ─── getAgentToModelMapForProfile ───────────────────────────────────────────── + +describe('getAgentToModelMapForProfile', () => { + test('returns correct models for balanced profile', () => { + const map = getAgentToModelMapForProfile('balanced'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-codebase-mapper'], 'haiku'); + assert.strictEqual(map['gsd-verifier'], 'sonnet'); + }); + + test('returns correct models for budget profile', () => { + const map = getAgentToModelMapForProfile('budget'); + assert.strictEqual(map['gsd-planner'], 'sonnet'); + assert.strictEqual(map['gsd-phase-researcher'], 'haiku'); + }); + + test('returns correct models for quality profile', () => { + const map = getAgentToModelMapForProfile('quality'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-executor'], 'opus'); + }); + + test('returns all agents in the map', () => { + const map = getAgentToModelMapForProfile('balanced'); + const agentCount = Object.keys(MODEL_PROFILES).length; + assert.strictEqual(Object.keys(map).length, agentCount); + }); +}); + +// ─── formatAgentToModelMapAsTable ───────────────────────────────────────────── + +describe('formatAgentToModelMapAsTable', () => { + test('produces a table with header and separator', () => { + const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' }; + const table = formatAgentToModelMapAsTable(map); + assert.ok(table.includes('Agent'), 'should have Agent header'); + assert.ok(table.includes('Model'), 'should have Model header'); + assert.ok(table.includes('─'), 'should have separator line'); + assert.ok(table.includes('gsd-planner'), 'should list agent'); + assert.ok(table.includes('opus'), 'should list model'); + }); + + test('pads columns correctly', () => { + const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' }; + const table = formatAgentToModelMapAsTable(map); + const lines = table.split('\n').filter(l => l.trim()); + // Separator line uses ┼, data/header lines use │ + const dataLines = lines.filter(l => l.includes('│')); + const pipePositions = dataLines.map(l => l.indexOf('│')); + const unique = [...new Set(pipePositions)]; + assert.strictEqual(unique.length, 1, 'all data lines should align on │'); + }); + + test('handles empty map', () => { + const table = formatAgentToModelMapAsTable({}); + assert.ok(table.includes('Agent'), 'should still have header'); + }); +}); diff --git a/tests/opencode-agent-conversion.test.cjs b/tests/opencode-agent-conversion.test.cjs deleted file mode 100644 index 6ba62b54b..000000000 --- a/tests/opencode-agent-conversion.test.cjs +++ /dev/null @@ -1,143 +0,0 @@ -/** - * OpenCode Agent Frontmatter Conversion Tests - * - * Validates that convertClaudeToOpencodeFrontmatter correctly converts - * agent frontmatter for OpenCode compatibility when isAgent: true. - * - * Bug: Without isAgent flag, the function strips name: (agents need it), - * keeps color:/skills:/tools: record (should strip), and doesn't add - * model: inherit / mode: subagent (required by OpenCode agents). - */ - -const { test, describe } = require('node:test'); -const assert = require('node:assert'); - -process.env.GSD_TEST_MODE = '1'; -const { convertClaudeToOpencodeFrontmatter } = require('../bin/install.js'); - -// Sample Claude agent frontmatter (matches actual GSD agent format) -const SAMPLE_AGENT = `--- -name: gsd-executor -description: Executes GSD plans with atomic commits -tools: Read, Write, Edit, Bash, Grep, Glob -color: yellow -skills: - - gsd-executor-workflow -# hooks: -# PostToolUse: -# - matcher: "Write|Edit" -# hooks: -# - type: command -# command: "npx eslint --fix $FILE 2>/dev/null || true" ---- - - -You are a GSD plan executor. -`; - -// Sample Claude command frontmatter (for comparison — commands work differently) -const SAMPLE_COMMAND = `--- -name: gsd-execute-phase -description: Execute all plans in a phase -allowed-tools: - - Read - - Write - - Bash ---- - -Execute the phase plan.`; - -describe('OpenCode agent conversion (isAgent: true)', () => { - test('keeps name: field for agents', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents'); - }); - - test('adds model: inherit', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('model: inherit'), 'model: inherit should be added'); - }); - - test('adds mode: subagent', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added'); - }); - - test('strips tools: field', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents'); - assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated'); - }); - - test('strips skills: array', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped'); - assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped'); - }); - - test('strips color: field', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents'); - }); - - test('strips commented hooks block', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped'); - assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped'); - }); - - test('keeps description: field', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept'); - }); - - test('preserves body content', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); - assert.ok(result.includes(''), 'body should be preserved'); - assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact'); - }); - - test('applies body text replacements', () => { - const agentWithClaudePaths = `--- -name: test-agent -description: Test -tools: Read ---- - -Read ~/.claude/agent-memory/ for context. -Use $HOME/.claude/skills/ for reference.`; - - const result = convertClaudeToOpencodeFrontmatter(agentWithClaudePaths, { isAgent: true }); - assert.ok(result.includes('~/.config/opencode/agent-memory/'), '~/.claude should be replaced'); - assert.ok(result.includes('$HOME/.config/opencode/skills/'), '$HOME/.claude should be replaced'); - }); -}); - -describe('OpenCode command conversion (isAgent: false, default)', () => { - test('strips name: field for commands', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands'); - }); - - test('does not add model: or mode: for commands', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); - const frontmatter = result.split('---')[1]; - assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands'); - assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands'); - }); - - test('keeps description: for commands', () => { - const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); - const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('description:'), 'description should be kept'); - }); -}); diff --git a/tests/path-replacement.test.cjs b/tests/path-replacement.test.cjs new file mode 100644 index 000000000..db925cc47 --- /dev/null +++ b/tests/path-replacement.test.cjs @@ -0,0 +1,100 @@ +/** + * GSD Tests - path replacement in install.js + * + * Verifies that global installs produce ~/ paths in .md files, + * never resolved absolute paths containing os.homedir(). + * Reproduces the bug where Windows installs write C:/Users/... + * paths that break in Docker containers. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const repoRoot = path.join(__dirname, '..'); + +// Simulate the pathPrefix computation from install.js (global install) +function computePathPrefix(homedir, targetDir) { + return path.resolve(targetDir).replace(homedir, '~').replace(/\\/g, '/') + '/'; +} + +describe('pathPrefix computation', () => { + test('default Claude global install uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.claude'); + const prefix = computePathPrefix(homedir, targetDir); + assert.strictEqual(prefix, '~/.claude/'); + }); + + test('default Gemini global install uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.gemini'); + const prefix = computePathPrefix(homedir, targetDir); + assert.strictEqual(prefix, '~/.gemini/'); + }); + + test('custom config dir under home uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.config', 'claude'); + const prefix = computePathPrefix(homedir, targetDir); + assert.ok(prefix.startsWith('~/'), `Expected ~/ prefix, got: ${prefix}`); + assert.ok(!prefix.includes(homedir), `Should not contain homedir: ${homedir}`); + }); + + test('Windows-style paths produce ~/ not C:/', () => { + // On Windows, path.resolve returns the input unchanged when it's already absolute. + // Simulate the string operation directly (can't use path.resolve for Windows paths on Linux). + const winHomedir = 'C:\\Users\\matte'; + const winTargetDir = 'C:\\Users\\matte\\.claude'; + // This is what the fix does: targetDir.replace(homedir, '~').replace(/\\/g, '/') + '/' + const prefix = winTargetDir.replace(winHomedir, '~').replace(/\\/g, '/') + '/'; + assert.strictEqual(prefix, '~/.claude/'); + assert.ok(!prefix.includes('C:'), `Should not contain drive letter, got: ${prefix}`); + }); +}); + +describe('installed .md files contain no resolved absolute paths', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.claude'); + const pathPrefix = computePathPrefix(homedir, targetDir); + const claudeDirRegex = /~\/\.claude\//g; + const claudeHomeRegex = /\$HOME\/\.claude\//g; + const normalizedHomedir = homedir.replace(/\\/g, '/'); + + // Collect all .md files from source directories + function collectMdFiles(dir) { + const results = []; + if (!fs.existsSync(dir)) return results; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...collectMdFiles(fullPath)); + } else if (entry.name.endsWith('.md')) { + results.push(fullPath); + } + } + return results; + } + + const dirsToCheck = ['commands', 'get-shit-done', 'agents'].map(d => path.join(repoRoot, d)); + const mdFiles = dirsToCheck.flatMap(collectMdFiles); + + test('source .md files exist', () => { + assert.ok(mdFiles.length > 0, `Expected .md files, found ${mdFiles.length}`); + }); + + test('after replacement, no .md file contains os.homedir()', () => { + const failures = []; + for (const file of mdFiles) { + let content = fs.readFileSync(file, 'utf8'); + content = content.replace(claudeDirRegex, pathPrefix); + content = content.replace(claudeHomeRegex, pathPrefix); + if (content.includes(normalizedHomedir) && normalizedHomedir !== '~') { + failures.push(path.relative(repoRoot, file)); + } + } + assert.deepStrictEqual(failures, [], `Files with resolved absolute paths: ${failures.join(', ')}`); + }); +}); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 76a21a68e..bbc0c004e 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1468,6 +1468,71 @@ describe('phase complete command', () => { const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); assert.ok(req.includes('- [ ] **AMT-01**'), 'AMT-01 should remain unchanged'); }); + + test('preserves Milestone column in 5-column progress table', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation + +### Phase 1: Foundation +**Goal:** Setup +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Foundation | v1.0 | 0/1 | Planned | | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + const rowMatch = roadmap.match(/^\|[^\n]*1\. Foundation[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v1.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should be Complete'); + }); + + test('updates STATE.md with plain format fields (no bold)', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 1: Only\n**Goal:** Test\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\nPhase: 1 of 1 (Only)\nStatus: In progress\nPlan: 01-01\nLast Activity: 2025-01-01\nLast Activity Description: Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-only'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('Milestone complete'), 'plain Status field should be updated'); + assert.ok(state.includes('Not started'), 'plain Plan field should be updated'); + // Verify compound format preserved + assert.ok(state.match(/Phase:.*of\s+1/), 'should preserve "of N" in compound Phase format'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/profile-output.test.cjs b/tests/profile-output.test.cjs new file mode 100644 index 000000000..3001195f5 --- /dev/null +++ b/tests/profile-output.test.cjs @@ -0,0 +1,197 @@ +/** + * Profile Output Tests + * + * Tests for profile rendering commands and PROFILING_QUESTIONS data. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs'); + +const { + PROFILING_QUESTIONS, + CLAUDE_INSTRUCTIONS, +} = require('../get-shit-done/bin/lib/profile-output.cjs'); + +// ─── PROFILING_QUESTIONS data ───────────────────────────────────────────────── + +describe('PROFILING_QUESTIONS', () => { + test('is a non-empty array', () => { + assert.ok(Array.isArray(PROFILING_QUESTIONS)); + assert.ok(PROFILING_QUESTIONS.length > 0); + }); + + test('each question has required fields', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok(q.dimension, `question missing dimension`); + assert.ok(q.header, `${q.dimension} missing header`); + assert.ok(q.question, `${q.dimension} missing question`); + assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`); + assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`); + } + }); + + test('each option has label, value, and rating', () => { + for (const q of PROFILING_QUESTIONS) { + for (const opt of q.options) { + assert.ok(opt.label, `${q.dimension} option missing label`); + assert.ok(opt.value, `${q.dimension} option missing value`); + assert.ok(opt.rating, `${q.dimension} option missing rating`); + } + } + }); + + test('all dimension keys are unique', () => { + const dims = PROFILING_QUESTIONS.map(q => q.dimension); + const unique = [...new Set(dims)]; + assert.strictEqual(dims.length, unique.length); + }); +}); + +// ─── CLAUDE_INSTRUCTIONS ────────────────────────────────────────────────────── + +describe('CLAUDE_INSTRUCTIONS', () => { + test('is a non-empty object', () => { + assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object'); + assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0); + }); + + test('each dimension has at least one instruction', () => { + for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) { + assert.ok(typeof instructions === 'object', `${dim} should be an object`); + assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`); + } + }); + + test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok( + CLAUDE_INSTRUCTIONS[q.dimension], + `${q.dimension} has questions but no CLAUDE_INSTRUCTIONS` + ); + } + }); +}); + +// ─── write-profile command ──────────────────────────────────────────────────── + +describe('write-profile command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('writes USER-PROFILE.md from analysis JSON', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + explanation_depth: { rating: 'concise', confidence: 'HIGH' }, + debugging_approach: { rating: 'fix-first', confidence: 'LOW' }, + ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' }, + vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' }, + frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' }, + learning_style: { rating: 'hands-on', confidence: 'MEDIUM' }, + }, + }; + + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.profile_path, 'should return profile_path'); + assert.ok(out.dimensions_scored > 0, 'should have scored dimensions'); + }); + + test('errors when --input is missing', () => { + const result = runGsdTools('write-profile --raw', tmpDir); + assert.ok(!result.success, 'should fail without --input'); + assert.ok(result.error.includes('--input'), 'should mention --input'); + }); +}); + +// ─── generate-claude-md command ─────────────────────────────────────────────── + +describe('generate-claude-md command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempGitProject(); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('generates CLAUDE.md with --auto flag', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + + if (fs.existsSync(outputPath)) { + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should have content'); + } + }); + + test('does not overwrite existing CLAUDE.md without --force', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n'); + + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + // Should merge, not overwrite + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should still have content'); + }); +}); + +// ─── generate-dev-preferences ───────────────────────────────────────────────── + +describe('generate-dev-preferences command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('errors when --analysis is missing', () => { + const result = runGsdTools('generate-dev-preferences --raw', tmpDir); + assert.ok(!result.success, 'should fail without --analysis'); + assert.ok(result.error.includes('--analysis'), 'should mention --analysis'); + }); + + test('generates preferences from analysis file', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + }, + }; + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.command_path || out.command_name, 'should return command output'); + }); +}); diff --git a/tests/profile-pipeline.test.cjs b/tests/profile-pipeline.test.cjs new file mode 100644 index 000000000..e6e783412 --- /dev/null +++ b/tests/profile-pipeline.test.cjs @@ -0,0 +1,160 @@ +/** + * Profile Pipeline Tests + * + * Tests for session scanning, message extraction, and profile sampling. + * Uses synthetic session data in temp directories via --path override. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── scan-sessions ──────────────────────────────────────────────────────────── + +describe('scan-sessions command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns empty array for empty sessions directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + fs.mkdirSync(sessionsDir, { recursive: true }); + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return an array'); + assert.strictEqual(out.length, 0, 'should be empty'); + }); + + test('scans synthetic project directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'test-project-abc123'); + fs.mkdirSync(projectDir, { recursive: true }); + + // Create a synthetic session file + const sessionData = [ + JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }), + JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }), + ].join('\n'); + fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData); + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return array'); + assert.strictEqual(out.length, 1, 'should find 1 project'); + assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session'); + }); + + test('reports multiple sessions and sizes', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'multi-session-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + for (let i = 1; i <= 3; i++) { + const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() }); + fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n'); + } + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out[0].sessionCount, 3); + assert.ok(out[0].totalSize > 0, 'should have non-zero size'); + }); +}); + +// ─── extract-messages ───────────────────────────────────────────────────────── + +describe('extract-messages command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('extracts user messages from synthetic session', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'my-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() }, + { type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', isMeta: true, message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages'); + assert.strictEqual(out.project, 'my-project'); + assert.ok(out.output_file, 'should have output file path'); + }); + + test('filters out meta and internal messages', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'filter-test'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages'); + }); +}); + +// ─── profile-questionnaire ──────────────────────────────────────────────────── + +describe('profile-questionnaire command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns questionnaire structure', () => { + const result = runGsdTools('profile-questionnaire --raw', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.questions, 'should have questions array'); + assert.ok(out.questions.length > 0, 'should have at least one question'); + assert.ok(out.questions[0].dimension, 'each question should have a dimension'); + assert.ok(out.questions[0].options, 'each question should have options'); + }); +}); diff --git a/tests/quick-branching.test.cjs b/tests/quick-branching.test.cjs new file mode 100644 index 000000000..5259ae199 --- /dev/null +++ b/tests/quick-branching.test.cjs @@ -0,0 +1,39 @@ +/** + * Quick task branching tests + * + * Validates that /gsd:quick exposes branch_name from init and that the + * workflow checks out a dedicated quick-task branch when configured. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +describe('quick workflow: branching support', () => { + const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'quick.md'); + let content; + + test('workflow file exists', () => { + assert.ok(fs.existsSync(workflowPath), 'workflows/quick.md should exist'); + }); + + test('init parse list includes branch_name', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok(content.includes('branch_name'), 'quick workflow should parse branch_name from init JSON'); + }); + + test('workflow includes quick-task branching step', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok(content.includes('Step 2.5: Handle quick-task branching')); + assert.ok(content.includes('git checkout -b "$branch_name" 2>/dev/null || git checkout "$branch_name"')); + }); + + test('branching step runs before task directory creation', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + const branchingIndex = content.indexOf('Step 2.5: Handle quick-task branching'); + const createDirIndex = content.indexOf('Step 3: Create task directory'); + assert.ok(branchingIndex !== -1 && createDirIndex !== -1, 'workflow should contain both branching and directory steps'); + assert.ok(branchingIndex < createDirIndex, 'branching should happen before quick task directories and commits'); + }); +}); diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index ea5e2f538..f043e40f9 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -756,6 +756,74 @@ describe('roadmap update-plan-progress command', () => { assert.strictEqual(output.updated, false, 'should not update'); assert.ok(output.reason.includes('ROADMAP.md not found'), 'reason should mention missing ROADMAP.md'); }); + + test('marks completed plan checkboxes', () => { + const roadmapContent = `# Roadmap + +- [ ] Phase 50: Build + - [ ] 50-01-PLAN.md + - [ ] 50-02-PLAN.md + +### Phase 50: Build +**Goal:** Build stuff +**Plans:** 2 plans + +## Progress + +| Phase | Plans Complete | Status | Completed | +|-------|---------------|--------|-----------| +| 50. Build | 0/2 | Planned | | +`; + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent); + + const p50 = path.join(tmpDir, '.planning', 'phases', '50-build'); + fs.mkdirSync(p50, { recursive: true }); + fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan 1'); + fs.writeFileSync(path.join(p50, '50-02-PLAN.md'), '# Plan 2'); + // Only plan 1 has a summary (completed) + fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary 1'); + + const result = runGsdTools('roadmap update-plan-progress 50', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('[x] 50-01-PLAN.md') || roadmap.includes('[x] 50-01'), + 'completed plan checkbox should be marked'); + assert.ok(roadmap.includes('[ ] 50-02-PLAN.md') || roadmap.includes('[ ] 50-02'), + 'incomplete plan checkbox should remain unchecked'); + }); + + test('preserves Milestone column in 5-column progress table', () => { + const roadmapContent = `# Roadmap + +### Phase 50: Build +**Goal:** Build stuff +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 50. Build | v2.0 | 0/1 | Planned | | +`; + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent); + + const p50 = path.join(tmpDir, '.planning', 'phases', '50-build'); + fs.mkdirSync(p50, { recursive: true }); + fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('roadmap update-plan-progress 50', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + const rowMatch = roadmap.match(/^\|[^\n]*50\. Build[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v2.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should show Complete'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs new file mode 100644 index 000000000..0dc33cd11 --- /dev/null +++ b/tests/runtime-converters.test.cjs @@ -0,0 +1,239 @@ +/** + * Runtime Converter Tests — OpenCode + Gemini + * + * Tests for small runtime-specific conversion functions from install.js. + * Larger runtime test suites (Copilot, Codex, Antigravity) have their own files. + * + * OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes) + * model: inherit is NOT added (OpenCode doesn't support it — see #1156) + * but mode: subagent IS added (required by OpenCode agents). + * Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping) + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); + +process.env.GSD_TEST_MODE = '1'; +const { + convertClaudeToOpencodeFrontmatter, + convertClaudeToGeminiAgent, + neutralizeAgentReferences, +} = require('../bin/install.js'); + +// Sample Claude agent frontmatter (matches actual GSD agent format) +const SAMPLE_AGENT = `--- +name: gsd-executor +description: Executes GSD plans with atomic commits +tools: Read, Write, Edit, Bash, Grep, Glob +color: yellow +skills: + - gsd-executor-workflow +# hooks: +# PostToolUse: +# - matcher: "Write|Edit" +# hooks: +# - type: command +# command: "npx eslint --fix $FILE 2>/dev/null || true" +--- + + +You are a GSD plan executor. +`; + +// Sample Claude command frontmatter (for comparison — commands work differently) +const SAMPLE_COMMAND = `--- +name: gsd-execute-phase +description: Execute all plans in a phase +allowed-tools: + - Read + - Write + - Bash +--- + +Execute the phase plan.`; + +describe('OpenCode agent conversion (isAgent: true)', () => { + test('keeps name: field for agents', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents'); + }); + + test('does not add model: inherit (OpenCode does not support it)', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError'); + }); + + test('adds mode: subagent', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added'); + }); + + test('strips tools: field', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents'); + assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated'); + }); + + test('strips skills: array', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped'); + assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped'); + }); + + test('strips color: field', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents'); + }); + + test('strips commented hooks block', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped'); + assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped'); + }); + + test('keeps description: field', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + const frontmatter = result.split('---')[1]; + assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept'); + }); + + test('preserves body content', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); + assert.ok(result.includes(''), 'body should be preserved'); + assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact'); + }); + + test('applies body text replacements', () => { + const agentWithClaudePaths = `--- +name: test-agent +description: Test +tools: Read +--- + +Read ~/.claude/agent-memory/ for context. +Use $HOME/.claude/skills/ for reference.`; + + const result = convertClaudeToOpencodeFrontmatter(agentWithClaudePaths, { isAgent: true }); + assert.ok(result.includes('~/.config/opencode/agent-memory/'), '~/.claude should be replaced'); + assert.ok(result.includes('$HOME/.config/opencode/skills/'), '$HOME/.claude should be replaced'); + }); +}); + +describe('OpenCode command conversion (isAgent: false, default)', () => { + test('strips name: field for commands', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands'); + }); + + test('does not add model: or mode: for commands', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); + const frontmatter = result.split('---')[1]; + assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands'); + assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands'); + }); + + test('keeps description: for commands', () => { + const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND); + const frontmatter = result.split('---')[1]; + assert.ok(frontmatter.includes('description:'), 'description should be kept'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Gemini CLI agent conversion (merged from gemini-config.test.cjs) +// ───────────────────────────────────────────────────────────────────────────── + +describe('convertClaudeToGeminiAgent', () => { + test('drops unsupported skills frontmatter while keeping converted tools', () => { + const input = `--- +name: gsd-codebase-mapper +description: Explores codebase and writes structured analysis documents. +tools: Read, Bash, Grep, Glob, Write +color: cyan +skills: + - gsd-mapper-workflow +--- + + +Use \${PHASE} in shell examples. +`; + + const result = convertClaudeToGeminiAgent(input); + const frontmatter = result.split('---')[1] || ''; + + assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); + assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); + assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); + assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); + assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); + assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); + assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); + assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); + assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); + assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); + assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); + assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); + assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); + }); +}); + +// ─── neutralizeAgentReferences (#766) ───────────────────────────────────────── + +describe('neutralizeAgentReferences', () => { + test('replaces standalone Claude with "the agent"', () => { + const input = 'Claude handles these decisions. Claude should read the file.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Claude handles'), 'standalone Claude replaced'); + assert.ok(result.includes('the agent handles'), 'replaced with "the agent"'); + }); + + test('preserves Claude Code (product name)', () => { + const input = 'This is a Claude Code bug. Use Claude Code settings.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Code bug'), 'Claude Code preserved'); + assert.ok(result.includes('Claude Code settings'), 'Claude Code preserved'); + }); + + test('preserves Claude model names', () => { + const input = 'Use Claude Opus for planning. Claude Sonnet for execution. Claude Haiku for research.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Opus'), 'Opus preserved'); + assert.ok(result.includes('Claude Sonnet'), 'Sonnet preserved'); + assert.ok(result.includes('Claude Haiku'), 'Haiku preserved'); + }); + + test('replaces CLAUDE.md with runtime instruction file', () => { + const input = 'Read CLAUDE.md for project instructions. Check ./CLAUDE.md if exists.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('AGENTS.md'), 'CLAUDE.md -> AGENTS.md'); + assert.ok(!result.includes('CLAUDE.md'), 'no CLAUDE.md remains'); + }); + + test('uses different instruction file per runtime', () => { + const input = 'Read CLAUDE.md for instructions.'; + assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md')); + assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md')); + assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md')); + }); + + test('removes AGENTS.md load-blocking instruction', () => { + const input = 'Do NOT load full `AGENTS.md` files — they contain agent definitions.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Do NOT load full'), 'blocking instruction removed'); + }); + + test('preserves claude- prefixes (CSS classes, package names)', () => { + const input = 'The claude-ctx session and claude-code package.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('claude-ctx'), 'claude- prefix preserved'); + assert.ok(result.includes('claude-code'), 'claude-code preserved'); + }); +}); diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 14ea38982..7f86f25fc 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -506,7 +506,7 @@ describe('STATE.md frontmatter sync', () => { // stateExtractField and stateReplaceField helpers // ───────────────────────────────────────────────────────────────────────────── -const { stateExtractField, stateReplaceField } = require('../get-shit-done/bin/lib/state.cjs'); +const { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('../get-shit-done/bin/lib/state.cjs'); describe('stateExtractField and stateReplaceField helpers', () => { // stateExtractField tests @@ -585,6 +585,45 @@ describe('stateExtractField and stateReplaceField helpers', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// stateReplaceFieldWithFallback — consolidated fallback helper +// ───────────────────────────────────────────────────────────────────────────── + +describe('stateReplaceFieldWithFallback', () => { + test('replaces primary field when present', () => { + const content = '# State\n\n**Status:** Old\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', null, 'New'); + assert.ok(result.includes('**Status:** New')); + }); + + test('falls back to secondary field when primary not found', () => { + const content = '# State\n\nLast activity: 2024-01-01\n'; + const result = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', '2025-03-19'); + assert.ok(result.includes('Last activity: 2025-03-19'), 'should update fallback field'); + }); + + test('returns content unchanged when neither field matches', () => { + const content = '# State\n\n**Phase:** 3\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', 'state', 'New'); + assert.strictEqual(result, content, 'content should be unchanged'); + }); + + test('prefers primary over fallback when both exist', () => { + const content = '# State\n\n**Status:** Old\nStatus: Also old\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', 'Status', 'New'); + // Bold format is tried first by stateReplaceField + assert.ok(result.includes('**Status:** New'), 'should replace bold (primary) format'); + }); + + test('works with plain format fields', () => { + const content = '# State\n\nPhase: 1 of 3 (Foundation)\nStatus: In progress\nPlan: 01-01\n'; + let updated = stateReplaceFieldWithFallback(content, 'Status', null, 'Complete'); + assert.ok(updated.includes('Status: Complete'), 'should update plain Status'); + updated = stateReplaceFieldWithFallback(updated, 'Current Plan', 'Plan', 'Not started'); + assert.ok(updated.includes('Plan: Not started'), 'should fall back to Plan field'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // cmdStateLoad, cmdStateGet, cmdStatePatch, cmdStateUpdate CLI tests // ───────────────────────────────────────────────────────────────────────────── @@ -892,6 +931,45 @@ describe('cmdStateAdvancePlan (state advance-plan)', () => { assert.ok(output.error !== undefined, 'output should have error field'); assert.ok(output.error.toLowerCase().includes('cannot parse'), 'error should mention Cannot parse'); }); + + test('advances plan in compound "Plan: X of Y" format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State\n\nPlan: 2 of 5 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n` + ); + + const result = runGsdTools('state advance-plan', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.advanced, true, 'advanced should be true'); + assert.strictEqual(output.previous_plan, 2); + assert.strictEqual(output.current_plan, 3); + assert.strictEqual(output.total_plans, 5); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Plan: 3 of 5 in current phase'), + 'should preserve compound format with updated plan number'); + assert.ok(updated.includes('Status: Ready to execute'), + 'Status should be updated'); + }); + + test('marks phase complete on last plan in compound format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State\n\nPlan: 3 of 3 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n` + ); + + const result = runGsdTools('state advance-plan', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.advanced, false); + assert.strictEqual(output.reason, 'last_plan'); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Phase complete'), 'Status should contain Phase complete'); + }); }); describe('cmdStateRecordMetric (state record-metric)', () => { diff --git a/tests/template.test.cjs b/tests/template.test.cjs new file mode 100644 index 000000000..8d2ae3d51 --- /dev/null +++ b/tests/template.test.cjs @@ -0,0 +1,186 @@ +/** + * Template Tests + * + * Tests for cmdTemplateSelect (heuristic template selection) and + * cmdTemplateFill (summary, plan, verification template generation). + */ + +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'); + +// ─── template select ────────────────────────────────────────────────────────── + +describe('template select command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create a phase directory with a plan + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('selects minimal template for simple plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Do the thing.', + '', + 'File: `src/index.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'minimal'); + assert.ok(out.template.includes('summary-minimal')); + }); + + test('selects standard template for moderate plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Create `src/auth/login.ts`', + '', + '### Task 2', + 'Create `src/auth/register.ts`', + '', + '### Task 3', + 'Update `src/routes/index.ts`', + '', + 'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + }); + + test('selects complex template for plan with decisions and many files', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + const lines = ['# Plan', '']; + for (let i = 1; i <= 6; i++) { + lines.push(`### Task ${i}`, `Do task ${i}.`, ''); + } + lines.push('Made a decision about architecture.', 'Another decision here.'); + for (let i = 1; i <= 8; i++) { + lines.push(`File: \`src/module${i}/index.ts\``); + } + fs.writeFileSync(planPath, lines.join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'complex'); + }); + + test('returns standard as fallback for nonexistent file', () => { + const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + assert.ok(out.error, 'should include error message'); + }); +}); + +// ─── template fill ──────────────────────────────────────────────────────────── + +describe('template fill command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('fills summary template', () => { + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-SUMMARY.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Phase 1'), 'should reference phase'); + assert.ok(content.includes('Accomplishments'), 'should have accomplishments section'); + }); + + test('fills plan template', () => { + const result = runGsdTools('template fill plan --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-PLAN.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Objective'), 'should have objective section'); + assert.ok(content.includes(''), 'should have task XML'); + }); + + test('fills verification template', () => { + const result = runGsdTools('template fill verification --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-VERIFICATION.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('Observable Truths'), 'should have truths section'); + assert.ok(content.includes('Required Artifacts'), 'should have artifacts section'); + }); + + test('rejects existing file', () => { + // Create the file first + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing'); + + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success); // outputs JSON, doesn't crash + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report error for existing file'); + assert.ok(out.error.includes('already exists')); + }); + + test('errors on unknown template type', () => { + const result = runGsdTools('template fill bogus --phase 1', tmpDir); + assert.ok(!result.success, 'should fail for unknown type'); + assert.ok(result.error.includes('Unknown template type')); + }); + + test('errors when phase not found', () => { + const result = runGsdTools('template fill summary --phase 99', tmpDir); + assert.ok(result.success); + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report phase not found'); + }); + + test('respects --plan option for plan number', () => { + const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`); + }); +}); diff --git a/tests/uat.test.cjs b/tests/uat.test.cjs new file mode 100644 index 000000000..3c3fbc424 --- /dev/null +++ b/tests/uat.test.cjs @@ -0,0 +1,326 @@ +/** + * GSD Tools Tests - UAT Audit + */ + +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('audit-uat command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns empty results when no UAT files exist', () => { + // Create a phase directory with no UAT files + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-foundation', '.gitkeep'), ''); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.deepStrictEqual(output.results, []); + assert.strictEqual(output.summary.total_items, 0); + assert.strictEqual(output.summary.total_files, 0); + }); + + test('detects UAT with pending items', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), `--- +status: testing +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Login Form +expected: Form displays with email and password fields +result: pass + +### 2. Submit Button +expected: Submitting shows loading state +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].phase, '01'); + assert.strictEqual(output.results[0].items[0].result, 'pending'); + assert.strictEqual(output.results[0].items[0].category, 'pending'); + assert.strictEqual(output.results[0].items[0].name, 'Submit Button'); + }); + + test('detects UAT with blocked items and categorizes blocked_by', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. API Health Check +expected: Returns 200 OK +result: blocked +blocked_by: server +reason: Server not running locally +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].items[0].result, 'blocked'); + assert.strictEqual(output.results[0].items[0].category, 'server_blocked'); + assert.strictEqual(output.results[0].items[0].blocked_by, 'server'); + }); + + test('detects false completion (complete status with pending items)', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), `--- +status: complete +phase: 03-ui +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Dashboard Layout +expected: Cards render in grid +result: pass + +### 2. Mobile Responsive +expected: Grid collapses to single column on mobile +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].status, 'complete'); + assert.strictEqual(output.results[0].items[0].result, 'pending'); + }); + + test('extracts human_needed items from VERIFICATION files', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-auth'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '04-VERIFICATION.md'), `--- +status: human_needed +phase: 04-auth +--- + +## Automated Checks + +All passed. + +## Human Verification + +1. Test SSO login with Google account +2. Test password reset flow end-to-end +3. Verify MFA enrollment on new device +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 3); + assert.strictEqual(output.results[0].type, 'verification'); + assert.strictEqual(output.results[0].status, 'human_needed'); + assert.strictEqual(output.results[0].items[0].category, 'human_uat'); + assert.strictEqual(output.results[0].items[0].name, 'Test SSO login with Google account'); + }); + + test('scans and aggregates across multiple phases', () => { + // Phase 1 with pending + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- +status: partial +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Test A +expected: Works +result: pending +`); + + // Phase 2 with blocked + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Test B +expected: Responds +result: blocked +blocked_by: server + +### 2. Test C +expected: Returns data +result: skipped +reason: device not available +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_files, 2); + assert.strictEqual(output.summary.total_items, 3); + assert.strictEqual(output.summary.by_phase['01'], 1); + assert.strictEqual(output.summary.by_phase['02'], 2); + }); + + test('milestone scoping filters phases to current milestone', () => { + // Create a ROADMAP.md that only references Phase 2 + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap + +### Phase 2: API Layer +**Goal:** Build API +`); + + // Phase 1 (not in current milestone) with pending + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- +status: partial +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Old Test +expected: Old behavior +result: pending +`); + + // Phase 2 (in current milestone) with pending + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. New Test +expected: New behavior +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Only Phase 2 should be included (Phase 1 not in ROADMAP) + assert.strictEqual(output.summary.total_files, 1); + assert.strictEqual(output.results[0].phase, '02'); + }); + + test('summary by_category counts are correct', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-billing'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '05-UAT.md'), `--- +status: partial +phase: 05-billing +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Payment Form +expected: Stripe elements load +result: pending + +### 2. Webhook Handler +expected: Processes payment events +result: blocked +blocked_by: third-party Stripe + +### 3. Invoice PDF +expected: Generates downloadable PDF +result: skipped +reason: needs release build + +### 4. Refund Flow +expected: Processes refund +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 4); + assert.strictEqual(output.summary.by_category.pending, 2); + assert.strictEqual(output.summary.by_category.third_party, 1); + assert.strictEqual(output.summary.by_category.build_needed, 1); + }); + + test('ignores VERIFICATION files without human_needed or gaps_found status', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '01-VERIFICATION.md'), `--- +status: passed +phase: 01-foundation +--- + +## Results + +All checks passed. +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 0); + assert.strictEqual(output.summary.total_files, 0); + }); +}); diff --git a/tests/verify-health.test.cjs b/tests/verify-health.test.cjs index c06a8404a..f22dd3ad7 100644 --- a/tests/verify-health.test.cjs +++ b/tests/verify-health.test.cjs @@ -191,10 +191,9 @@ describe('validate health command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); - assert.ok( - output.warnings.some(w => w.code === 'W002'), - `Expected W002 in warnings: ${JSON.stringify(output.warnings)}` - ); + const w002 = output.warnings.find(w => w.code === 'W002'); + assert.ok(w002, `Expected W002 in warnings: ${JSON.stringify(output.warnings)}`); + assert.strictEqual(w002.repairable, false, 'W002 should not be auto-repairable'); }); // ─── Check 5: config.json valid JSON + valid schema ─────────────────────── @@ -613,16 +612,13 @@ describe('validate health --repair command', () => { assert.ok(stateContent.includes('# Session State'), 'regenerated STATE.md should contain "# Session State"'); }); - test('backs up existing STATE.md before regenerating', () => { + test('does not rewrite existing STATE.md for invalid phase references', () => { writeValidConfigJson(tmpDir); const statePath = path.join(tmpDir, '.planning', 'STATE.md'); - const originalContent = '# Session State\n\nOriginal content here.\n'; - fs.writeFileSync(statePath, originalContent); - - // Make STATE.md reference a nonexistent phase so repair is triggered + const originalContent = '# Session State\n\nPhase 99 is current.\n'; fs.writeFileSync( statePath, - '# Session State\n\nPhase 99 is current.\n' + originalContent ); const result = runGsdTools('validate health --repair', tmpDir); @@ -630,19 +626,17 @@ describe('validate health --repair command', () => { const output = JSON.parse(result.output); assert.ok( - Array.isArray(output.repairs_performed), - `Expected repairs_performed: ${JSON.stringify(output)}` + !Array.isArray(output.repairs_performed) || !output.repairs_performed.some(r => r.action === 'regenerateState'), + `Did not expect regenerateState for W002: ${JSON.stringify(output)}` ); - // Verify a .bak- file exists alongside STATE.md + const stateContent = fs.readFileSync(statePath, 'utf-8'); + assert.strictEqual(stateContent, originalContent, 'existing STATE.md should be preserved'); + const planningDir = path.join(tmpDir, '.planning'); const planningFiles = fs.readdirSync(planningDir); const backupFile = planningFiles.find(f => f.startsWith('STATE.md.bak-')); - assert.ok(backupFile, `Expected a STATE.md.bak- file. Found files: ${planningFiles.join(', ')}`); - - // Verify backup contains the original content - const backupContent = fs.readFileSync(path.join(planningDir, backupFile), 'utf-8'); - assert.ok(backupContent.includes('Phase 99'), 'backup should contain the original STATE.md content'); + assert.strictEqual(backupFile, undefined, `Did not expect backup file for non-destructive repair. Found: ${planningFiles.join(', ')}`); }); test('adds nyquist_validation key to config.json via addNyquistKey repair', () => { @@ -688,4 +682,18 @@ describe('validate health --repair command', () => { `Expected repairable_count >= 2, got ${output.repairable_count}. Full output: ${JSON.stringify(output)}` ); }); + + test('phase mismatch warnings do not count as repairable issues', () => { + writeValidConfigJson(tmpDir); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Session State\n\nPhase 99 is the current phase.\n' + ); + + const result = runGsdTools('validate health', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.repairable_count, 0, `Expected no repairable issues for W002: ${JSON.stringify(output)}`); + }); });