merge: resolve conflicts with upstream main
VALID_CONFIG_KEYS: merge our additions (workflow.auto_advance,
workflow.node_repair, workflow.node_repair_budget, hooks.context_warnings)
with upstream's additions (workflow.text_mode, git.quick_branch_template).
ensureConfigFile(): keep our refactored version that delegates to
buildNewProjectConfig({}) instead of upstream's duplicated logic.
buildNewProjectConfig(): add git.quick_branch_template: null and
workflow.text_mode: false to match upstream's new keys.
new-project.md: integrate upstream's Step 5.1 Sub-Repo Detection
after our commit block; drop upstream's duplicate Note (ours at
line 493 is more detailed).
This commit is contained in:
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node-version: [18, 20, 22]
|
||||
node-version: [20, 22, 24]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
@@ -37,13 +37,5 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests with coverage
|
||||
# c8 v11 requires Node 20+ (engines: ^20.0.0 || >=22.0.0). Node 18 EOL April 2025.
|
||||
# Use bash on all platforms so shell glob expansion works on Windows.
|
||||
if: matrix.node-version != 18
|
||||
shell: bash
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Run tests (Node 18, coverage not supported)
|
||||
if: matrix.node-version == 18
|
||||
shell: bash
|
||||
run: npm test
|
||||
|
||||
51
.release-monitor.sh
Executable file
51
.release-monitor.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Release monitor for gsd-build/get-shit-done
|
||||
# Checks every 15 minutes, writes new release info to a signal file
|
||||
|
||||
REPO="gsd-build/get-shit-done"
|
||||
SIGNAL_FILE="/tmp/gsd-new-release.json"
|
||||
STATE_FILE="/tmp/gsd-monitor-last-tag"
|
||||
LOG_FILE="/tmp/gsd-monitor.log"
|
||||
|
||||
# Initialize with current latest
|
||||
echo "v1.25.1" > "$STATE_FILE"
|
||||
rm -f "$SIGNAL_FILE"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log "Monitor started. Watching $REPO for releases newer than v1.25.1"
|
||||
log "Checking every 15 minutes..."
|
||||
|
||||
while true; do
|
||||
sleep 900 # 15 minutes
|
||||
|
||||
LAST_KNOWN=$(cat "$STATE_FILE" 2>/dev/null)
|
||||
|
||||
# Get latest release tag
|
||||
LATEST=$(gh release list -R "$REPO" --limit 1 2>/dev/null | awk '{print $1}')
|
||||
|
||||
if [ -z "$LATEST" ]; then
|
||||
log "WARNING: Failed to fetch releases (network issue?)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$LATEST" != "$LAST_KNOWN" ]; then
|
||||
log "NEW RELEASE DETECTED: $LATEST (was: $LAST_KNOWN)"
|
||||
|
||||
# Fetch release notes
|
||||
RELEASE_BODY=$(gh release view "$LATEST" -R "$REPO" --json tagName,name,body 2>/dev/null)
|
||||
|
||||
# Write signal file for the agent to pick up
|
||||
echo "$RELEASE_BODY" > "$SIGNAL_FILE"
|
||||
echo "$LATEST" > "$STATE_FILE"
|
||||
|
||||
log "Signal file written to $SIGNAL_FILE"
|
||||
# Exit so the agent can process it, then restart
|
||||
exit 0
|
||||
else
|
||||
log "No new release. Latest is still $LATEST"
|
||||
fi
|
||||
done
|
||||
51
CHANGELOG.md
51
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
|
||||
|
||||
21
README.md
21
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)
|
||||
|
||||
[](https://www.npmjs.com/package/get-shit-done-cc)
|
||||
[](https://www.npmjs.com/package/get-shit-done-cc)
|
||||
[](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml)
|
||||
@@ -342,19 +344,26 @@ If everything passes, you move on. If something's broken, you don't manually deb
|
||||
|
||||
---
|
||||
|
||||
### 6. Repeat → Complete → Next Milestone
|
||||
### 6. Repeat → Ship → Complete → Next Milestone
|
||||
|
||||
```
|
||||
/gsd:discuss-phase 2
|
||||
/gsd:plan-phase 2
|
||||
/gsd:execute-phase 2
|
||||
/gsd:verify-work 2
|
||||
/gsd:ship 2 # Create PR from verified work
|
||||
...
|
||||
/gsd:complete-milestone
|
||||
/gsd:new-milestone
|
||||
```
|
||||
|
||||
Loop **discuss → plan → execute → verify** until milestone complete.
|
||||
Or let GSD figure out the next step automatically:
|
||||
|
||||
```
|
||||
/gsd:next # Auto-detect and run next step
|
||||
```
|
||||
|
||||
Loop **discuss → plan → execute → verify → ship** until milestone complete.
|
||||
|
||||
If you want faster intake during discussion, use `/gsd:discuss-phase <n> --batch` to answer a small grouped set of questions at once instead of one-by-one.
|
||||
|
||||
@@ -491,6 +500,8 @@ You're never locked in. The system adapts.
|
||||
| `/gsd:plan-phase [N] [--auto]` | Research + plan + verify for a phase |
|
||||
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves, verify when complete |
|
||||
| `/gsd:verify-work [N]` | Manual user acceptance testing ¹ |
|
||||
| `/gsd:ship [N] [--draft]` | Create PR from verified phase work with auto-generated body |
|
||||
| `/gsd:next` | Automatically advance to the next logical workflow step |
|
||||
| `/gsd:audit-milestone` | Verify milestone achieved its definition of done |
|
||||
| `/gsd:complete-milestone` | Archive milestone, tag release |
|
||||
| `/gsd:new-milestone [name]` | Start next version: questions → research → requirements → roadmap |
|
||||
@@ -507,6 +518,7 @@ You're never locked in. The system adapts.
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd:progress` | Where am I? What's next? |
|
||||
| `/gsd:next` | Auto-detect state and run the next step |
|
||||
| `/gsd:help` | Show all commands and usage guide |
|
||||
| `/gsd:update` | Update GSD with changelog preview |
|
||||
| `/gsd:join-discord` | Join the GSD Discord community |
|
||||
@@ -531,8 +543,9 @@ You're never locked in. The system adapts.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd:pause-work` | Create handoff when stopping mid-phase |
|
||||
| `/gsd:pause-work` | Create handoff when stopping mid-phase (writes HANDOFF.json) |
|
||||
| `/gsd:resume-work` | Restore from last session |
|
||||
| `/gsd:session-report` | Generate session summary with work performed and outcomes |
|
||||
|
||||
### Utilities
|
||||
|
||||
@@ -581,7 +594,7 @@ Switch profiles:
|
||||
/gsd:set-profile budget
|
||||
```
|
||||
|
||||
Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`).
|
||||
Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`).
|
||||
|
||||
Or configure via `/gsd:settings`.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `plans`, `incomplete_plans`.
|
||||
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `plans`, `incomplete_plans`.
|
||||
|
||||
Also read STATE.md for position, decisions, blockers:
|
||||
```bash
|
||||
@@ -328,6 +328,14 @@ git add src/types/user.ts
|
||||
| `chore` | Config, tooling, dependencies |
|
||||
|
||||
**4. Commit:**
|
||||
|
||||
**If `sub_repos` is configured (non-empty array from init context):** Use `commit-to-subrepo` to route files to their correct sub-repo:
|
||||
```bash
|
||||
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {concise task description}" --files file1 file2 ...
|
||||
```
|
||||
Returns JSON with per-repo commit hashes: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`. Record all hashes for SUMMARY.
|
||||
|
||||
**Otherwise (standard single-repo):**
|
||||
```bash
|
||||
git commit -m "{type}({phase}-{plan}): {concise task description}
|
||||
|
||||
@@ -336,7 +344,9 @@ git commit -m "{type}({phase}-{plan}): {concise task description}
|
||||
"
|
||||
```
|
||||
|
||||
**5. Record hash:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY.
|
||||
**5. Record hash:**
|
||||
- **Single-repo:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY.
|
||||
- **Multi-repo (sub_repos):** Extract hashes from `commit-to-subrepo` JSON output (`repos.{name}.hash`). Record all hashes for SUMMARY (e.g., `backend@abc1234, frontend@def5678`).
|
||||
|
||||
**6. Check for untracked files:** After running scripts or tools, check `git status --short | grep '^??'`. For any new untracked files: commit if intentional, add to `.gitignore` if generated/runtime output. Never leave generated files untracked.
|
||||
</task_commit_protocol>
|
||||
|
||||
@@ -194,6 +194,7 @@ Priority: Context7 > Official Docs > Official GitHub > Verified WebSearch > Unve
|
||||
- [ ] Publication dates checked (prefer recent/current)
|
||||
- [ ] Confidence levels assigned honestly
|
||||
- [ ] "What might I have missed?" review completed
|
||||
- [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank)
|
||||
|
||||
</verification_protocol>
|
||||
|
||||
@@ -274,6 +275,20 @@ src/
|
||||
|
||||
**Key insight:** [why custom solutions are worse in this domain]
|
||||
|
||||
## Runtime State Inventory
|
||||
|
||||
> Include this section for rename/refactor/migration phases only. Omit entirely for greenfield phases.
|
||||
|
||||
| Category | Items Found | Action Required |
|
||||
|----------|-------------|------------------|
|
||||
| Stored data | [e.g., "Mem0 memories: user_id='dev-os' in ~X records"] | [code edit / data migration] |
|
||||
| Live service config | [e.g., "25 n8n workflows in SQLite not exported to git"] | [API patch / manual] |
|
||||
| OS-registered state | [e.g., "Windows Task Scheduler: 3 tasks with 'dev-os' in description"] | [re-register tasks] |
|
||||
| Secrets/env vars | [e.g., "SOPS key 'webhook_auth_header' — code rename only, key unchanged"] | [none / update key] |
|
||||
| Build artifacts | [e.g., "scripts/devos-cli/devos_cli.egg-info/ — stale after pyproject.toml rename"] | [reinstall package] |
|
||||
|
||||
**Nothing found in category:** State explicitly ("None — verified by X").
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: [Name]
|
||||
@@ -407,6 +422,26 @@ Based on phase description, identify what needs investigating:
|
||||
- **Pitfalls:** Common beginner mistakes, gotchas, rewrite-causing errors
|
||||
- **Don't Hand-Roll:** Existing solutions for deceptively complex problems
|
||||
|
||||
## Step 2.5: Runtime State Inventory (rename / refactor / migration phases only)
|
||||
|
||||
**Trigger:** Any phase involving rename, rebrand, refactor, string replacement, or migration.
|
||||
|
||||
A grep audit finds files. It does NOT find runtime state. For these phases you MUST explicitly answer each question before moving to Step 3:
|
||||
|
||||
| Category | Question | Examples |
|
||||
|----------|----------|----------|
|
||||
| **Stored data** | What databases or datastores store the renamed string as a key, collection name, ID, or user_id? | ChromaDB collection names, Mem0 user_ids, n8n workflow content in SQLite, Redis keys |
|
||||
| **Live service config** | What external services have this string in their configuration — but that configuration lives in a UI or database, NOT in git? | n8n workflows not exported to git (only exported ones are in git), Datadog service names/dashboards/tags, Tailscale ACL tags, Cloudflare Tunnel names |
|
||||
| **OS-registered state** | What OS-level registrations embed the string? | Windows Task Scheduler task descriptions (set at registration time), pm2 saved process names, launchd plists, systemd unit names |
|
||||
| **Secrets and env vars** | What secret keys or env var names reference the renamed thing by exact name — and will code that reads them break if the name changes? | SOPS key names, .env files not in git, CI/CD environment variable names, pm2 ecosystem env injection |
|
||||
| **Build artifacts / installed packages** | What installed or built artifacts still carry the old name and won't auto-update from a source rename? | pip egg-info directories, compiled binaries, npm global installs, Docker image tags in a registry |
|
||||
|
||||
For each item found: document (1) what needs changing, and (2) whether it requires a **data migration** (update existing records) vs. a **code edit** (change how new records are written). These are different tasks and must both appear in the plan.
|
||||
|
||||
**The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?*
|
||||
|
||||
If the answer for a category is "nothing" — say so explicitly. Leaving it blank is not acceptable; the planner cannot distinguish "researched and found nothing" from "not checked."
|
||||
|
||||
## Step 3: Execute Research Protocol
|
||||
|
||||
For each domain: Context7 first → Official docs → WebSearch → Cross-verify. Document findings with confidence levels as you go.
|
||||
@@ -460,7 +495,7 @@ List missing test files, framework config, or shared fixtures needed before impl
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
|----|-------------|------------------|
|
||||
| {REQ-ID} | {from REQUIREMENTS.md} | {which research findings enable implementation} |
|
||||
</phase_requirements>
|
||||
```
|
||||
@@ -556,4 +591,4 @@ Quality indicators:
|
||||
- **Actionable:** Planner could create tasks based on this research
|
||||
- **Current:** Year included in searches, publication dates checked
|
||||
|
||||
</success_criteria>
|
||||
</success_criteria>
|
||||
450
bin/install.js
450
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 <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`);
|
||||
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -551,6 +567,8 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
|
||||
c = c.replace(/\.claude\//g, '.github/');
|
||||
// CONV-07: Command name conversion (all gsd: references → gsd-)
|
||||
c = c.replace(/gsd:/g, 'gsd-');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
c = neutralizeAgentReferences(c, 'copilot-instructions.md');
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -641,6 +659,8 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) {
|
||||
c = c.replace(/\.claude\//g, '.agent/');
|
||||
// Command name conversion (all gsd: references → gsd-)
|
||||
c = c.replace(/gsd:/g, 'gsd-');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
c = neutralizeAgentReferences(c, 'GEMINI.md');
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -694,6 +714,14 @@ function yamlQuote(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function yamlIdentifier(value) {
|
||||
const text = String(value).trim();
|
||||
if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return yamlQuote(text);
|
||||
}
|
||||
|
||||
function extractFrontmatterAndBody(content) {
|
||||
if (!content.startsWith('---')) {
|
||||
return { frontmatter: null, body: content };
|
||||
@@ -717,6 +745,121 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
// Tool name mapping from Claude Code to Cursor CLI
|
||||
const claudeToCursorTools = {
|
||||
Bash: 'Shell',
|
||||
Edit: 'StrReplace',
|
||||
AskUserQuestion: null, // No direct equivalent — use conversational prompting
|
||||
SlashCommand: null, // No equivalent — skills are auto-discovered
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Claude Code tool name to Cursor CLI format
|
||||
* @returns {string|null} Cursor tool name, or null if tool should be excluded
|
||||
*/
|
||||
function convertCursorToolName(claudeTool) {
|
||||
if (claudeTool in claudeToCursorTools) {
|
||||
return claudeToCursorTools[claudeTool];
|
||||
}
|
||||
// MCP tools keep their format (Cursor supports MCP)
|
||||
if (claudeTool.startsWith('mcp__')) {
|
||||
return claudeTool;
|
||||
}
|
||||
// Most tools share the same name (Read, Write, Glob, Grep, Task, WebSearch, WebFetch, TodoWrite)
|
||||
return claudeTool;
|
||||
}
|
||||
|
||||
function convertSlashCommandsToCursorSkillMentions(content) {
|
||||
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
|
||||
// This preserves rendered "next step" commands like "/gsd-execute-phase 17".
|
||||
return content.replace(/gsd:/gi, 'gsd-');
|
||||
}
|
||||
|
||||
function convertClaudeToCursorMarkdown(content) {
|
||||
let converted = convertSlashCommandsToCursorSkillMentions(content);
|
||||
// Replace tool name references in body text
|
||||
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
||||
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
||||
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
||||
// Replace subagent_type from Claude to Cursor format
|
||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||
// Replace project-level Claude conventions with Cursor equivalents
|
||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`');
|
||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/');
|
||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`');
|
||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/');
|
||||
converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
||||
// Remove Claude Code-specific bug workarounds before brand replacement
|
||||
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
||||
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
||||
// Replace "Claude Code" brand references with "Cursor"
|
||||
converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
|
||||
return converted;
|
||||
}
|
||||
|
||||
function getCursorSkillAdapterHeader(skillName) {
|
||||
return `<cursor_skill_adapter>
|
||||
## A. Skill Invocation
|
||||
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
||||
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
|
||||
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
|
||||
|
||||
## B. User Prompting
|
||||
When the workflow needs user input, prompt the user conversationally:
|
||||
- Present options as a numbered list in your response text
|
||||
- Ask the user to reply with their choice
|
||||
- For multi-select, ask for comma-separated numbers
|
||||
|
||||
## C. Tool Usage
|
||||
Use these Cursor tools when executing GSD workflows:
|
||||
- \`Shell\` for running commands (terminal operations)
|
||||
- \`StrReplace\` for editing existing files
|
||||
- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
|
||||
|
||||
## D. Subagent Spawning
|
||||
When the workflow needs to spawn a subagent:
|
||||
- Use \`Task(subagent_type="generalPurpose", ...)\`
|
||||
- The \`model\` parameter maps to Cursor's model options (e.g., "fast")
|
||||
</cursor_skill_adapter>`;
|
||||
}
|
||||
|
||||
function convertClaudeCommandToCursorSkill(content, skillName) {
|
||||
const converted = convertClaudeToCursorMarkdown(content);
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
let description = `Run GSD workflow ${skillName}.`;
|
||||
if (frontmatter) {
|
||||
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
||||
if (maybeDescription) {
|
||||
description = maybeDescription;
|
||||
}
|
||||
}
|
||||
description = toSingleLine(description);
|
||||
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
||||
const adapter = getCursorSkillAdapterHeader(skillName);
|
||||
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Claude Code agent markdown to Cursor agent format.
|
||||
* Strips frontmatter fields Cursor doesn't support (color, skills),
|
||||
* converts tool references, and adds a role context header.
|
||||
*/
|
||||
function convertClaudeAgentToCursorAgent(content) {
|
||||
let converted = convertClaudeToCursorMarkdown(content);
|
||||
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
if (!frontmatter) return converted;
|
||||
|
||||
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
||||
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
|
||||
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
||||
|
||||
return `${cleanFrontmatter}\n${body}`;
|
||||
}
|
||||
|
||||
function convertSlashCommandsToCodexSkillMentions(content) {
|
||||
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
|
||||
return `$gsd-${String(commandName).toLowerCase()}`;
|
||||
@@ -728,6 +871,8 @@ function convertSlashCommandsToCodexSkillMentions(content) {
|
||||
function convertClaudeToCodexMarkdown(content) {
|
||||
let converted = convertSlashCommandsToCodexSkillMentions(content);
|
||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
converted = neutralizeAgentReferences(converted, 'AGENTS.md');
|
||||
return converted;
|
||||
}
|
||||
|
||||
@@ -819,14 +964,22 @@ purpose: ${toSingleLine(description)}
|
||||
|
||||
/**
|
||||
* Generate a per-agent .toml config file for Codex.
|
||||
* Sets sandbox_mode and developer_instructions from the agent markdown body.
|
||||
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
||||
* from the agent markdown content.
|
||||
*/
|
||||
function generateCodexAgentToml(agentName, agentContent) {
|
||||
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
||||
const { body } = extractFrontmatterAndBody(agentContent);
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
||||
const frontmatterText = frontmatter || '';
|
||||
const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
|
||||
const resolvedDescription = toSingleLine(
|
||||
extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}`
|
||||
);
|
||||
const instructions = body.trim();
|
||||
|
||||
const lines = [
|
||||
`name = ${JSON.stringify(resolvedName)}`,
|
||||
`description = ${JSON.stringify(resolvedDescription)}`,
|
||||
`sandbox_mode = "${sandboxMode}"`,
|
||||
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
||||
// TOML literal multiline strings preserve them without escape parsing.
|
||||
@@ -857,6 +1010,10 @@ function generateCodexConfigBlock(agents) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function stripCodexGsdAgentSections(content) {
|
||||
return content.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip GSD sections from Codex config.toml content.
|
||||
* Returns cleaned content, or null if file would be empty.
|
||||
@@ -882,7 +1039,7 @@ function stripGsdFromCodexConfig(content) {
|
||||
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
|
||||
|
||||
// Remove [agents.gsd-*] sections (from header to next section or EOF)
|
||||
cleaned = cleaned.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
||||
cleaned = stripCodexGsdAgentSections(cleaned);
|
||||
|
||||
// Remove [features] section if now empty (only header, no keys before next section)
|
||||
cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
||||
@@ -916,7 +1073,7 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
||||
let before = existing.substring(0, markerIndex).trimEnd();
|
||||
if (before) {
|
||||
// Strip any GSD-managed sections that leaked above the marker from previous installs
|
||||
before = before.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
||||
before = stripCodexGsdAgentSections(before);
|
||||
before = before.replace(/^\[agents\]\n(?:(?!\[)[^\n]*\n?)*/m, '');
|
||||
before = before.replace(/\n{3,}/g, '\n\n').trimEnd();
|
||||
|
||||
@@ -929,7 +1086,14 @@ function mergeCodexConfig(configPath, gsdBlock) {
|
||||
|
||||
// Case 3: No marker — append GSD block
|
||||
let content = existing;
|
||||
content = content.trimEnd() + '\n\n' + gsdBlock + '\n';
|
||||
content = stripCodexGsdAgentSections(content);
|
||||
content = content.replace(/\n{3,}/g, '\n\n').trimEnd();
|
||||
|
||||
if (content) {
|
||||
content = content + '\n\n' + gsdBlock + '\n';
|
||||
} else {
|
||||
content = gsdBlock + '\n';
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, content);
|
||||
}
|
||||
@@ -1036,6 +1200,36 @@ function installCodexConfig(targetDir, agentsSrc) {
|
||||
* Terminals don't support subscript — Gemini renders these as raw HTML.
|
||||
* Converts <sub>text</sub> to italic *(text)* for readable terminal output.
|
||||
*/
|
||||
/**
|
||||
* Runtime-neutral agent name and instruction file replacement.
|
||||
* Used by ALL non-Claude runtime converters to avoid Claude-specific
|
||||
* references in workflow prompts, agent definitions, and documentation.
|
||||
*
|
||||
* Replaces:
|
||||
* - Standalone "Claude" (agent name) → "the agent"
|
||||
* Preserves: "Claude Code" (product), "Claude Opus/Sonnet/Haiku" (models),
|
||||
* "claude-" (prefixes), "CLAUDE.md" (handled separately)
|
||||
* - "CLAUDE.md" → runtime-appropriate instruction file
|
||||
* - "Do NOT load full AGENTS.md" → removed (harmful for AGENTS.md runtimes)
|
||||
*
|
||||
* @param {string} content - File content to neutralize
|
||||
* @param {string} instructionFile - Runtime's instruction file ('AGENTS.md', 'GEMINI.md', etc.)
|
||||
* @returns {string} Content with runtime-neutral references
|
||||
*/
|
||||
function neutralizeAgentReferences(content, instructionFile) {
|
||||
let c = content;
|
||||
// Replace standalone "Claude" (the agent) but preserve product/model names.
|
||||
// Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based
|
||||
c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent');
|
||||
// Replace CLAUDE.md with runtime-appropriate instruction file
|
||||
if (instructionFile) {
|
||||
c = c.replace(/CLAUDE\.md/g, instructionFile);
|
||||
}
|
||||
// Remove instructions that conflict with AGENTS.md-based runtimes
|
||||
c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, '');
|
||||
return c;
|
||||
}
|
||||
|
||||
function stripSubTags(content) {
|
||||
return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
|
||||
}
|
||||
@@ -1140,7 +1334,9 @@ function convertClaudeToGeminiAgent(content) {
|
||||
// is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
|
||||
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
||||
|
||||
return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`;
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md');
|
||||
return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`;
|
||||
}
|
||||
|
||||
function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
@@ -1156,6 +1352,8 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode');
|
||||
// Replace general-purpose subagent type with OpenCode's equivalent "general"
|
||||
convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
|
||||
|
||||
// Check if content has frontmatter
|
||||
if (!convertedContent.startsWith('---')) {
|
||||
@@ -1228,6 +1426,13 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip model: field — OpenCode doesn't support Claude Code model aliases
|
||||
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
|
||||
// its configured default model. See #1156.
|
||||
if (trimmed.startsWith('model:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert color names to hex for opencode (commands only; agents strip color above)
|
||||
if (trimmed.startsWith('color:')) {
|
||||
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
||||
@@ -1264,8 +1469,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
}
|
||||
|
||||
// For agents: add required OpenCode agent fields
|
||||
// Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
|
||||
// keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
|
||||
// use its default model for subagents. See #1156.
|
||||
if (isAgent) {
|
||||
newLines.push('model: inherit');
|
||||
newLines.push('mode: subagent');
|
||||
}
|
||||
|
||||
@@ -1445,6 +1652,59 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(skillsDir, { recursive: true });
|
||||
|
||||
// Remove previous GSD Cursor skills to avoid stale command skills
|
||||
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of existing) {
|
||||
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(currentSrcDir, currentPrefix) {
|
||||
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(currentSrcDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.name.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseName = entry.name.replace('.md', '');
|
||||
const skillName = `${currentPrefix}-${baseName}`;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
const globalClaudeRegex = /~\/\.claude\//g;
|
||||
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
||||
const localClaudeRegex = /\.\/\.claude\//g;
|
||||
const cursorDirRegex = /~\/\.cursor\//g;
|
||||
content = content.replace(globalClaudeRegex, pathPrefix);
|
||||
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
||||
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
||||
content = content.replace(cursorDirRegex, pathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
content = convertClaudeCommandToCursorSkill(content, skillName);
|
||||
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
||||
}
|
||||
}
|
||||
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
|
||||
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
|
||||
@@ -1561,6 +1821,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Clean install: remove existing destination to prevent orphaned files
|
||||
@@ -1617,6 +1878,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
content = convertClaudeToAntigravityContent(content, isGlobal);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (isCursor) {
|
||||
content = convertClaudeToCursorMarkdown(content);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else {
|
||||
fs.writeFileSync(destPath, content);
|
||||
}
|
||||
@@ -1630,6 +1894,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
content = convertClaudeToAntigravityContent(content, isGlobal);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
||||
// For Cursor, also convert Claude references in JS/CJS utility scripts
|
||||
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
||||
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
|
||||
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/');
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
@@ -1722,6 +1994,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Get the target directory based on runtime and install type
|
||||
@@ -1739,6 +2012,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
if (runtime === 'codex') runtimeLabel = 'Codex';
|
||||
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
||||
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
||||
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
||||
|
||||
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -1765,8 +2039,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
console.log(` ${green}✓${reset} Removed GSD commands from command/`);
|
||||
}
|
||||
} else if (isCodex) {
|
||||
// Codex: remove skills/gsd-*/SKILL.md skill directories
|
||||
} else if (isCodex || isCursor) {
|
||||
// Codex/Cursor: remove skills/gsd-*/SKILL.md skill directories
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
@@ -1779,11 +2053,12 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} ${runtimeLabel} skills`);
|
||||
}
|
||||
}
|
||||
|
||||
// Codex: remove GSD agent .toml config files
|
||||
// Codex-only: remove GSD agent .toml config files and config.toml sections
|
||||
if (isCodex) {
|
||||
const codexAgentsDir = path.join(targetDir, 'agents');
|
||||
if (fs.existsSync(codexAgentsDir)) {
|
||||
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
||||
@@ -1816,6 +2091,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isCopilot) {
|
||||
// Copilot: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
@@ -1866,6 +2142,23 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
|
||||
}
|
||||
}
|
||||
} else if (isCursor) {
|
||||
// Cursor: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
skillCount++;
|
||||
}
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
||||
if (fs.existsSync(gsdCommandsDir)) {
|
||||
@@ -2274,6 +2567,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const gsdDir = path.join(configDir, 'get-shit-done');
|
||||
const commandsDir = path.join(configDir, 'commands', 'gsd');
|
||||
const opencodeCommandDir = path.join(configDir, 'command');
|
||||
@@ -2285,7 +2579,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
for (const [rel, hash] of Object.entries(gsdHashes)) {
|
||||
manifest.files['get-shit-done/' + rel] = hash;
|
||||
}
|
||||
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && fs.existsSync(commandsDir)) {
|
||||
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && fs.existsSync(commandsDir)) {
|
||||
const cmdHashes = generateManifest(commandsDir);
|
||||
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
||||
manifest.files['commands/gsd/' + rel] = hash;
|
||||
@@ -2298,7 +2592,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((isCodex || isCopilot || isAntigravity) && fs.existsSync(codexSkillsDir)) {
|
||||
if ((isCodex || isCopilot || isAntigravity || isCursor) && fs.existsSync(codexSkillsDir)) {
|
||||
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
||||
const skillRoot = path.join(codexSkillsDir, skillName);
|
||||
const skillHashes = generateManifest(skillRoot);
|
||||
@@ -2314,6 +2608,18 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track hook files so saveLocalPatches() can detect user modifications
|
||||
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot)
|
||||
if (!isCodex && !isCopilot) {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
for (const file of fs.readdirSync(hooksDir)) {
|
||||
if (file.startsWith('gsd-') && file.endsWith('.js')) {
|
||||
manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
||||
return manifest;
|
||||
@@ -2376,7 +2682,9 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
||||
? '/gsd-reapply-patches'
|
||||
: runtime === 'codex'
|
||||
? '$gsd-reapply-patches'
|
||||
: '/gsd:reapply-patches';
|
||||
: runtime === 'cursor'
|
||||
? 'gsd-reapply-patches (mention the skill name)'
|
||||
: '/gsd:reapply-patches';
|
||||
console.log('');
|
||||
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
||||
for (const f of meta.files) {
|
||||
@@ -2397,6 +2705,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
@@ -2411,9 +2720,12 @@ function install(isGlobal, runtime = 'claude') {
|
||||
|
||||
// Path prefix for file references in markdown content (e.g. gsd-tools.cjs).
|
||||
// Replaces $HOME/.claude/ or ~/.claude/ so the result is <pathPrefix>get-shit-done/bin/...
|
||||
// Always use absolute path so: (1) local installs work when GSD is outside $HOME,
|
||||
// (2) spawned subagents with empty $HOME still resolve the path (fixes #820).
|
||||
const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`;
|
||||
// For global installs: use ~/ so paths work across environments (e.g. Docker
|
||||
// containers mounting ~/.claude from a Windows host where os.homedir() differs).
|
||||
// For local installs: use resolved absolute path (may be outside $HOME).
|
||||
const pathPrefix = isGlobal
|
||||
? path.resolve(targetDir).replace(os.homedir(), '~').replace(/\\/g, '/') + '/'
|
||||
: `${path.resolve(targetDir).replace(/\\/g, '/')}/`;
|
||||
|
||||
let runtimeLabel = 'Claude Code';
|
||||
if (isOpencode) runtimeLabel = 'OpenCode';
|
||||
@@ -2421,6 +2733,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
if (isCodex) runtimeLabel = 'Codex';
|
||||
if (isCopilot) runtimeLabel = 'Copilot';
|
||||
if (isAntigravity) runtimeLabel = 'Antigravity';
|
||||
if (isCursor) runtimeLabel = 'Cursor';
|
||||
|
||||
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -2488,6 +2801,16 @@ function install(isGlobal, runtime = 'claude') {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else if (isCursor) {
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
const gsdSrc = path.join(src, 'commands', 'gsd');
|
||||
copyCommandsAsCursorSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
|
||||
const installedSkillNames = listCodexSkillNames(skillsDir); // reuse — same dir structure
|
||||
if (installedSkillNames.length > 0) {
|
||||
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else {
|
||||
// Claude Code & Gemini: nested structure in commands/ directory
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
@@ -2552,6 +2875,8 @@ function install(isGlobal, runtime = 'claude') {
|
||||
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
||||
} else if (isAntigravity) {
|
||||
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
|
||||
} else if (isCursor) {
|
||||
content = convertClaudeAgentToCursorAgent(content);
|
||||
}
|
||||
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
||||
fs.writeFileSync(path.join(agentsDest, destName), content);
|
||||
@@ -2585,7 +2910,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
failures.push('VERSION');
|
||||
}
|
||||
|
||||
if (!isCodex && !isCopilot) {
|
||||
if (!isCodex && !isCopilot && !isCursor) {
|
||||
// Write package.json to force CommonJS mode for GSD scripts
|
||||
// Prevents "require is not defined" errors when project has "type": "module"
|
||||
// Node.js walks up looking for package.json - this stops inheritance from project
|
||||
@@ -2606,10 +2931,14 @@ function install(isGlobal, runtime = 'claude') {
|
||||
if (fs.statSync(srcFile).isFile()) {
|
||||
const destFile = path.join(hooksDest, entry);
|
||||
// Template .js files to replace '.claude' with runtime-specific config dir
|
||||
// and stamp the current GSD version into the hook version header
|
||||
if (entry.endsWith('.js')) {
|
||||
let content = fs.readFileSync(srcFile, 'utf8');
|
||||
content = content.replace(/'\.claude'/g, configDirReplacement);
|
||||
content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
|
||||
fs.writeFileSync(destFile, content);
|
||||
// Ensure hook files are executable (fixes #1162 — missing +x permission)
|
||||
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
|
||||
} else {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
}
|
||||
@@ -2689,6 +3018,60 @@ function install(isGlobal, runtime = 'claude') {
|
||||
const agentCount = installCodexConfig(targetDir, agentsSrc);
|
||||
console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`);
|
||||
console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`);
|
||||
|
||||
// Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag
|
||||
const configPath = path.join(targetDir, 'config.toml');
|
||||
try {
|
||||
let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
|
||||
|
||||
// Enable hooks feature flag if not present
|
||||
if (!configContent.includes('codex_hooks')) {
|
||||
if (configContent.includes('[features]')) {
|
||||
// Insert codex_hooks = true right after the [features] header.
|
||||
// Fixes #1202: previous approach could leave non-boolean keys (like
|
||||
// model = "gpt-5.4") under [features], causing Codex TOML parse errors.
|
||||
configContent = configContent.replace(/(\[features\]\n)/, '$1codex_hooks = true\n');
|
||||
} else {
|
||||
configContent = '[features]\ncodex_hooks = true\n\n' + configContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: detect non-boolean keys under [features] that would break Codex (#1202).
|
||||
// Extract the [features] section content (between [features] and next [section] or EOF).
|
||||
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
|
||||
if (featuresMatch) {
|
||||
const featuresBody = featuresMatch[1];
|
||||
const nonBooleanKeys = featuresBody.split('\n')
|
||||
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
|
||||
.map(line => line.trim());
|
||||
if (nonBooleanKeys.length > 0) {
|
||||
// Move non-boolean keys above [features] to prevent TOML parse errors
|
||||
let cleanedFeatures = featuresBody.split('\n')
|
||||
.filter(line => !line.match(/^\s*\w+\s*=/) || line.match(/=\s*(true|false)\s*(#.*)?$/))
|
||||
.join('\n');
|
||||
const movedKeys = nonBooleanKeys.join('\n') + '\n';
|
||||
configContent = configContent.replace(
|
||||
/\[features\]\n[\s\S]*?(?=\n\[|$)/,
|
||||
movedKeys + '\n[features]\n' + cleanedFeatures.trim() + '\n'
|
||||
);
|
||||
console.log(` ${yellow}⚠${reset} Moved ${nonBooleanKeys.length} non-feature key(s) out of [features] section to prevent TOML errors`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add SessionStart hook for update checking
|
||||
const updateCheckScript = path.resolve(targetDir, 'get-shit-done', 'hooks', 'gsd-update-check.js').replace(/\\/g, '/');
|
||||
const hookBlock = `\n# GSD Hooks\n[[hooks]]\nevent = "SessionStart"\ncommand = "node ${updateCheckScript}"\n`;
|
||||
|
||||
if (!configContent.includes('gsd-update-check')) {
|
||||
configContent += hookBlock;
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, configContent, 'utf-8');
|
||||
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart)`);
|
||||
} catch (e) {
|
||||
console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
|
||||
}
|
||||
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
}
|
||||
|
||||
@@ -2705,6 +3088,11 @@ function install(isGlobal, runtime = 'claude') {
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
// Cursor uses skills — no config.toml, no settings.json hooks needed
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
}
|
||||
|
||||
// Configure statusline and hooks in settings.json
|
||||
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
||||
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
||||
@@ -2788,8 +3176,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isCursor = runtime === 'cursor';
|
||||
|
||||
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot) {
|
||||
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor) {
|
||||
settings.statusLine = {
|
||||
type: 'command',
|
||||
command: statuslineCommand
|
||||
@@ -2798,7 +3187,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
}
|
||||
|
||||
// Write settings when runtime supports settings.json
|
||||
if (!isCodex && !isCopilot) {
|
||||
if (!isCodex && !isCopilot && !isCursor) {
|
||||
writeSettings(settingsPath, settings);
|
||||
}
|
||||
|
||||
@@ -2813,12 +3202,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
if (runtime === 'codex') program = 'Codex';
|
||||
if (runtime === 'copilot') program = 'Copilot';
|
||||
if (runtime === 'antigravity') program = 'Antigravity';
|
||||
if (runtime === 'cursor') program = 'Cursor';
|
||||
|
||||
let command = '/gsd:new-project';
|
||||
if (runtime === 'opencode') command = '/gsd-new-project';
|
||||
if (runtime === 'codex') command = '$gsd-new-project';
|
||||
if (runtime === 'copilot') command = '/gsd-new-project';
|
||||
if (runtime === 'antigravity') command = '/gsd-new-project';
|
||||
if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)';
|
||||
console.log(`
|
||||
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
|
||||
|
||||
@@ -2902,15 +3293,18 @@ function promptRuntime(callback) {
|
||||
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
|
||||
${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
|
||||
${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
||||
${cyan}7${reset}) All
|
||||
${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset}
|
||||
${cyan}8${reset}) All
|
||||
`);
|
||||
|
||||
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
||||
answered = true;
|
||||
rl.close();
|
||||
const choice = answer.trim() || '1';
|
||||
if (choice === '7') {
|
||||
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']);
|
||||
if (choice === '8') {
|
||||
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']);
|
||||
} else if (choice === '7') {
|
||||
callback(['cursor']);
|
||||
} else if (choice === '6') {
|
||||
callback(['antigravity']);
|
||||
} else if (choice === '5') {
|
||||
@@ -3010,7 +3404,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
||||
// Test-only exports — skip main logic when loaded as a module for testing
|
||||
if (process.env.GSD_TEST_MODE) {
|
||||
module.exports = {
|
||||
yamlIdentifier,
|
||||
getCodexSkillAdapterHeader,
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeToGeminiAgent,
|
||||
convertClaudeAgentToCodexAgent,
|
||||
generateCodexAgentToml,
|
||||
@@ -3020,6 +3417,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
installCodexConfig,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
neutralizeAgentReferences,
|
||||
GSD_CODEX_MARKER,
|
||||
CODEX_AGENT_SANDBOX,
|
||||
getDirName,
|
||||
|
||||
76
commands/gsd/add-backlog.md
Normal file
76
commands/gsd/add-backlog.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: gsd:add-backlog
|
||||
description: Add an idea to the backlog parking lot (999.x numbering)
|
||||
argument-hint: <description>
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add a backlog item to the roadmap using 999.x numbering. Backlog items are
|
||||
unsequenced ideas that aren't ready for active planning — they live outside
|
||||
the normal phase sequence and accumulate context over time.
|
||||
</objective>
|
||||
|
||||
<process>
|
||||
|
||||
1. **Read ROADMAP.md** to find existing backlog entries:
|
||||
```bash
|
||||
cat .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
2. **Find next backlog number:**
|
||||
```bash
|
||||
NEXT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 999 --raw)
|
||||
```
|
||||
If no 999.x phases exist, start at 999.1.
|
||||
|
||||
3. **Create the phase directory:**
|
||||
```bash
|
||||
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
|
||||
mkdir -p ".planning/phases/${NEXT}-${SLUG}"
|
||||
touch ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
|
||||
```
|
||||
|
||||
4. **Add to ROADMAP.md** under a `## Backlog` section. If the section doesn't exist, create it at the end:
|
||||
|
||||
```markdown
|
||||
## Backlog
|
||||
|
||||
### Phase {NEXT}: {description} (BACKLOG)
|
||||
|
||||
**Goal:** [Captured for future planning]
|
||||
**Requirements:** TBD
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
```
|
||||
|
||||
5. **Commit:**
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
|
||||
```
|
||||
|
||||
6. **Report:**
|
||||
```
|
||||
## 📋 Backlog Item Added
|
||||
|
||||
Phase {NEXT}: {description}
|
||||
Directory: .planning/phases/{NEXT}-{slug}/
|
||||
|
||||
This item lives in the backlog parking lot.
|
||||
Use /gsd:discuss-phase {NEXT} to explore it further.
|
||||
Use /gsd:review-backlog to promote items to active milestone.
|
||||
```
|
||||
|
||||
</process>
|
||||
|
||||
<notes>
|
||||
- 999.x numbering keeps backlog items out of the active phase sequence
|
||||
- Phase directories are created immediately, so /gsd:discuss-phase and /gsd:plan-phase work on them
|
||||
- No `Depends on:` field — backlog items are unsequenced by definition
|
||||
- Sparse numbering is fine (999.1, 999.3) — always uses next-decimal
|
||||
</notes>
|
||||
24
commands/gsd/audit-uat.md
Normal file
24
commands/gsd/audit-uat.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: gsd:audit-uat
|
||||
description: Cross-phase audit of all outstanding UAT and verification items
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
---
|
||||
<objective>
|
||||
Scan all phases for pending, skipped, blocked, and human_needed UAT items. Cross-reference against codebase to detect stale documentation. Produce prioritized human test plan.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/audit-uat.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Core planning files are loaded in-workflow via CLI.
|
||||
|
||||
**Scope:**
|
||||
Glob: .planning/phases/*/*-UAT.md
|
||||
Glob: .planning/phases/*/*-VERIFICATION.md
|
||||
</context>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:discuss-phase
|
||||
description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults).
|
||||
argument-hint: "<phase> [--auto]"
|
||||
argument-hint: "<phase> [--auto] [--batch] [--analyze]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:execute-phase
|
||||
description: Execute all plans in a phase with wave-based parallelization
|
||||
argument-hint: "<phase-number> [--gaps-only]"
|
||||
argument-hint: "<phase-number> [--gaps-only] [--interactive]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -31,6 +31,7 @@ Phase: $ARGUMENTS
|
||||
|
||||
**Flags:**
|
||||
- `--gaps-only` — Execute only gap closure plans (plans with `gap_closure: true` in frontmatter). Use after verify-work creates fix plans.
|
||||
- `--interactive` — Execute plans sequentially inline (no subagents) with user checkpoints between tasks. Lower token usage, pair-programming style. Best for small phases, bug fixes, and verification gaps.
|
||||
|
||||
Context files are resolved inside the workflow via `gsd-tools init execute-phase` and per-subagent `<files_to_read>` blocks.
|
||||
</context>
|
||||
|
||||
30
commands/gsd/fast.md
Normal file
30
commands/gsd/fast.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: gsd:fast
|
||||
description: Execute a trivial task inline — no subagents, no planning overhead
|
||||
argument-hint: "[task description]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
---
|
||||
|
||||
<objective>
|
||||
Execute a trivial task directly in the current context without spawning subagents
|
||||
or generating PLAN.md files. For tasks too small to justify planning overhead:
|
||||
typo fixes, config changes, small refactors, forgotten commits, simple additions.
|
||||
|
||||
This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that
|
||||
needs research, multi-step planning, or verification. /gsd:fast is for tasks
|
||||
you could describe in one sentence and execute in under 2 minutes.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/fast.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end.
|
||||
</process>
|
||||
24
commands/gsd/next.md
Normal file
24
commands/gsd/next.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: gsd:next
|
||||
description: Automatically advance to the next logical step in the GSD workflow
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- SlashCommand
|
||||
---
|
||||
<objective>
|
||||
Detect the current project state and automatically invoke the next logical GSD workflow step.
|
||||
No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next.
|
||||
|
||||
Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/next.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end.
|
||||
</process>
|
||||
28
commands/gsd/plant-seed.md
Normal file
28
commands/gsd/plant-seed.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: gsd:plant-seed
|
||||
description: Capture a forward-looking idea with trigger conditions — surfaces automatically at the right milestone
|
||||
argument-hint: "[idea summary]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
<objective>
|
||||
Capture an idea that's too big for now but should surface automatically when the right
|
||||
milestone arrives. Seeds solve context rot: instead of a one-liner in Deferred that nobody
|
||||
reads, a seed preserves the full WHY, WHEN to surface, and breadcrumbs to details.
|
||||
|
||||
Creates: .planning/seeds/SEED-NNN-slug.md
|
||||
Consumed by: /gsd:new-milestone (scans seeds and presents matches)
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/plant-seed.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the plant-seed workflow from @~/.claude/get-shit-done/workflows/plant-seed.md end-to-end.
|
||||
</process>
|
||||
25
commands/gsd/pr-branch.md
Normal file
25
commands/gsd/pr-branch.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: gsd:pr-branch
|
||||
description: Create a clean PR branch by filtering out .planning/ commits — ready for code review
|
||||
argument-hint: "[target branch, default: main]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create a clean branch suitable for pull requests by filtering out .planning/ commits
|
||||
from the current branch. Reviewers see only code changes, not GSD planning artifacts.
|
||||
|
||||
This solves the problem of PR diffs being cluttered with PLAN.md, SUMMARY.md, STATE.md
|
||||
changes that are irrelevant to code review.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/pr-branch.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the pr-branch workflow from @~/.claude/get-shit-done/workflows/pr-branch.md end-to-end.
|
||||
</process>
|
||||
61
commands/gsd/review-backlog.md
Normal file
61
commands/gsd/review-backlog.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: gsd:review-backlog
|
||||
description: Review and promote backlog items to active milestone
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
---
|
||||
|
||||
<objective>
|
||||
Review all 999.x backlog items and optionally promote them into the active
|
||||
milestone sequence or remove stale entries.
|
||||
</objective>
|
||||
|
||||
<process>
|
||||
|
||||
1. **List backlog items:**
|
||||
```bash
|
||||
ls -d .planning/phases/999* 2>/dev/null || echo "No backlog items found"
|
||||
```
|
||||
|
||||
2. **Read ROADMAP.md** and extract all 999.x phase entries:
|
||||
```bash
|
||||
cat .planning/ROADMAP.md
|
||||
```
|
||||
Show each backlog item with its description, any accumulated context (CONTEXT.md, RESEARCH.md), and creation date.
|
||||
|
||||
3. **Present the list to the user** via AskUserQuestion:
|
||||
- For each backlog item, show: phase number, description, accumulated artifacts
|
||||
- Options per item: **Promote** (move to active), **Keep** (leave in backlog), **Remove** (delete)
|
||||
|
||||
4. **For items to PROMOTE:**
|
||||
- Find the next sequential phase number in the active milestone
|
||||
- Rename the directory from `999.x-slug` to `{new_num}-slug`:
|
||||
```bash
|
||||
NEW_NUM=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase add "${DESCRIPTION}" --raw)
|
||||
```
|
||||
- Move accumulated artifacts to the new phase directory
|
||||
- Update ROADMAP.md: move the entry from `## Backlog` section to the active phase list
|
||||
- Remove `(BACKLOG)` marker
|
||||
- Add appropriate `**Depends on:**` field
|
||||
|
||||
5. **For items to REMOVE:**
|
||||
- Delete the phase directory
|
||||
- Remove the entry from ROADMAP.md `## Backlog` section
|
||||
|
||||
6. **Commit changes:**
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: review backlog — promoted N, removed M" --files .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
7. **Report summary:**
|
||||
```
|
||||
## 📋 Backlog Review Complete
|
||||
|
||||
Promoted: {list of promoted items with new phase numbers}
|
||||
Kept: {list of items remaining in backlog}
|
||||
Removed: {list of deleted items}
|
||||
```
|
||||
|
||||
</process>
|
||||
37
commands/gsd/review.md
Normal file
37
commands/gsd/review.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: gsd:review
|
||||
description: Request cross-AI peer review of phase plans from external AI CLIs
|
||||
argument-hint: "--phase N [--gemini] [--claude] [--codex] [--all]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
<objective>
|
||||
Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans.
|
||||
Produces a structured REVIEWS.md with per-reviewer feedback that can be fed back into
|
||||
planning via /gsd:plan-phase --reviews.
|
||||
|
||||
**Flow:** Detect CLIs → Build review prompt → Invoke each CLI → Collect responses → Write REVIEWS.md
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/review.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
Phase number: extracted from $ARGUMENTS (required)
|
||||
|
||||
**Flags:**
|
||||
- `--gemini` — Include Gemini CLI review
|
||||
- `--claude` — Include Claude CLI review (uses separate session)
|
||||
- `--codex` — Include Codex CLI review
|
||||
- `--all` — Include all available CLIs
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the review workflow from @~/.claude/get-shit-done/workflows/review.md end-to-end.
|
||||
</process>
|
||||
19
commands/gsd/session-report.md
Normal file
19
commands/gsd/session-report.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: gsd:session-report
|
||||
description: Generate a session report with token usage estimates, work summary, and outcomes
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Write
|
||||
---
|
||||
<objective>
|
||||
Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/session-report.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end.
|
||||
</process>
|
||||
23
commands/gsd/ship.md
Normal file
23
commands/gsd/ship.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: gsd:ship
|
||||
description: Create PR, run review, and prepare for merge after verification passes
|
||||
argument-hint: "[phase number or milestone, e.g., '4' or 'v1.0']"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
- Write
|
||||
- AskUserQuestion
|
||||
---
|
||||
<objective>
|
||||
Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge.
|
||||
|
||||
Closes the plan → execute → verify → ship loop.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/ship.md
|
||||
</execution_context>
|
||||
|
||||
Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end.
|
||||
127
commands/gsd/thread.md
Normal file
127
commands/gsd/thread.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: gsd:thread
|
||||
description: Manage persistent context threads for cross-session work
|
||||
argument-hint: [name | description]
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create, list, or resume persistent context threads. Threads are lightweight
|
||||
cross-session knowledge stores for work that spans multiple sessions but
|
||||
doesn't belong to any specific phase.
|
||||
</objective>
|
||||
|
||||
<process>
|
||||
|
||||
**Parse $ARGUMENTS to determine mode:**
|
||||
|
||||
<mode_list>
|
||||
**If no arguments or $ARGUMENTS is empty:**
|
||||
|
||||
List all threads:
|
||||
```bash
|
||||
ls .planning/threads/*.md 2>/dev/null
|
||||
```
|
||||
|
||||
For each thread, read the first few lines to show title and status:
|
||||
```
|
||||
## Active Threads
|
||||
|
||||
| Thread | Status | Last Updated |
|
||||
|--------|--------|-------------|
|
||||
| fix-deploy-key-auth | OPEN | 2026-03-15 |
|
||||
| pasta-tcp-timeout | RESOLVED | 2026-03-12 |
|
||||
| perf-investigation | IN PROGRESS | 2026-03-17 |
|
||||
```
|
||||
|
||||
If no threads exist, show:
|
||||
```
|
||||
No threads found. Create one with: /gsd:thread <description>
|
||||
```
|
||||
</mode_list>
|
||||
|
||||
<mode_resume>
|
||||
**If $ARGUMENTS matches an existing thread name (file exists):**
|
||||
|
||||
Resume the thread — load its context into the current session:
|
||||
```bash
|
||||
cat ".planning/threads/${THREAD_NAME}.md"
|
||||
```
|
||||
|
||||
Display the thread content and ask what the user wants to work on next.
|
||||
Update the thread's status to `IN PROGRESS` if it was `OPEN`.
|
||||
</mode_resume>
|
||||
|
||||
<mode_create>
|
||||
**If $ARGUMENTS is a new description (no matching thread file):**
|
||||
|
||||
Create a new thread:
|
||||
|
||||
1. Generate slug from description:
|
||||
```bash
|
||||
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
|
||||
```
|
||||
|
||||
2. Create the threads directory if needed:
|
||||
```bash
|
||||
mkdir -p .planning/threads
|
||||
```
|
||||
|
||||
3. Write the thread file:
|
||||
```bash
|
||||
cat > ".planning/threads/${SLUG}.md" << 'EOF'
|
||||
# Thread: {description}
|
||||
|
||||
## Status: OPEN
|
||||
|
||||
## Goal
|
||||
|
||||
{description}
|
||||
|
||||
## Context
|
||||
|
||||
*Created from conversation on {today's date}.*
|
||||
|
||||
## References
|
||||
|
||||
- *(add links, file paths, or issue numbers)*
|
||||
|
||||
## Next Steps
|
||||
|
||||
- *(what the next session should do first)*
|
||||
EOF
|
||||
```
|
||||
|
||||
4. If there's relevant context in the current conversation (code snippets,
|
||||
error messages, investigation results), extract and add it to the Context
|
||||
section.
|
||||
|
||||
5. Commit:
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create thread — ${ARGUMENTS}" --files ".planning/threads/${SLUG}.md"
|
||||
```
|
||||
|
||||
6. Report:
|
||||
```
|
||||
## 🧵 Thread Created
|
||||
|
||||
Thread: {slug}
|
||||
File: .planning/threads/{slug}.md
|
||||
|
||||
Resume anytime with: /gsd:thread {slug}
|
||||
```
|
||||
</mode_create>
|
||||
|
||||
</process>
|
||||
|
||||
<notes>
|
||||
- Threads are NOT phase-scoped — they exist independently of the roadmap
|
||||
- Lighter weight than /gsd:pause-work — no phase state, no plan context
|
||||
- The value is in Context and Next Steps — a cold-start session can pick up immediately
|
||||
- Threads can be promoted to phases or backlog items when they mature:
|
||||
/gsd:add-phase or /gsd:add-backlog with context from the thread
|
||||
- Thread files live in .planning/threads/ — no collision with phases or other GSD structures
|
||||
</notes>
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
`gsd-tools.cjs` is a Node.js CLI utility that replaces repetitive inline bash patterns across GSD's ~50 command, workflow, and agent files. It centralizes: config parsing, model resolution, phase lookup, git commits, summary verification, state management, and template operations.
|
||||
|
||||
**Location:** `get-shit-done/bin/gsd-tools.cjs`
|
||||
**Modules:** 11 domain modules in `get-shit-done/bin/lib/`
|
||||
**Modules:** 15 domain modules in `get-shit-done/bin/lib/`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
@@ -330,8 +330,14 @@ node gsd-tools.cjs progress [json|table|bar]
|
||||
# Complete a todo
|
||||
node gsd-tools.cjs todo complete <filename>
|
||||
|
||||
# UAT audit — scan all phases for unresolved items
|
||||
node gsd-tools.cjs audit-uat
|
||||
|
||||
# Git commit with config checks
|
||||
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend]
|
||||
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend] [--no-verify]
|
||||
```
|
||||
|
||||
> **`--no-verify`**: Skips pre-commit hooks. Used by parallel executor agents during wave-based execution to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator runs hooks once after each wave completes. Do not use `--no-verify` during sequential execution — let hooks run normally.
|
||||
|
||||
# Web search (requires Brave API key)
|
||||
node gsd-tools.cjs websearch <query> [--limit N] [--freshness day|week|month]
|
||||
@@ -355,3 +361,6 @@ node gsd-tools.cjs websearch <query> [--limit N] [--freshness day|week|month]
|
||||
| Milestone | `lib/milestone.cjs` | Milestone archival, requirements marking |
|
||||
| Commands | `lib/commands.cjs` | Misc: slug, timestamp, todos, scaffold, stats, websearch |
|
||||
| Model Profiles | `lib/model-profiles.cjs` | Profile resolution table |
|
||||
| UAT | `lib/uat.cjs` | Cross-phase UAT/verification audit |
|
||||
| Profile Output | `lib/profile-output.cjs` | Developer profile formatting |
|
||||
| Profile Pipeline | `lib/profile-pipeline.cjs` | Session analysis pipeline |
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
240
docs/FEATURES.md
240
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 <quality|balanced|budget|inherit>`
|
||||
|
||||
@@ -527,6 +639,7 @@
|
||||
- REQ-MODEL-02: Each profile MUST define model tier per agent (see profile table)
|
||||
- REQ-MODEL-03: Per-agent overrides MUST take precedence over profile
|
||||
- REQ-MODEL-04: `inherit` profile MUST defer to runtime's current model selection
|
||||
- REQ-MODEL-04a: `inherit` profile MUST be used when running non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs
|
||||
- REQ-MODEL-05: Profile switch MUST be programmatic (script, not LLM-driven)
|
||||
- REQ-MODEL-06: Model resolution MUST happen once per orchestration, not per spawn
|
||||
|
||||
@@ -551,7 +664,7 @@
|
||||
|
||||
## Brownfield Features
|
||||
|
||||
### 23. Codebase Mapping
|
||||
### 27. Codebase Mapping
|
||||
|
||||
**Command:** `/gsd:map-codebase [area]`
|
||||
|
||||
@@ -579,7 +692,7 @@
|
||||
|
||||
## Utility Features
|
||||
|
||||
### 24. Debug System
|
||||
### 28. Debug System
|
||||
|
||||
**Command:** `/gsd:debug [description]`
|
||||
|
||||
@@ -597,7 +710,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 25. Todo Management
|
||||
### 29. Todo Management
|
||||
|
||||
**Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos`
|
||||
|
||||
@@ -611,7 +724,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 26. Statistics Dashboard
|
||||
### 30. Statistics Dashboard
|
||||
|
||||
**Command:** `/gsd:stats`
|
||||
|
||||
@@ -625,7 +738,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 27. Update System
|
||||
### 31. Update System
|
||||
|
||||
**Command:** `/gsd:update`
|
||||
|
||||
@@ -640,7 +753,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 28. Settings Management
|
||||
### 32. Settings Management
|
||||
|
||||
**Command:** `/gsd:settings`
|
||||
|
||||
@@ -673,7 +786,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 29. Test Generation
|
||||
### 33. Test Generation
|
||||
|
||||
**Command:** `/gsd:add-tests [N]`
|
||||
|
||||
@@ -688,7 +801,7 @@
|
||||
|
||||
## Infrastructure Features
|
||||
|
||||
### 30. Git Integration
|
||||
### 34. Git Integration
|
||||
|
||||
**Purpose:** Atomic commits, branching strategies, and clean history management.
|
||||
|
||||
@@ -714,7 +827,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 31. CLI Tools
|
||||
### 35. CLI Tools
|
||||
|
||||
**Purpose:** Programmatic utilities for workflows and agents, replacing repetitive inline bash patterns.
|
||||
|
||||
@@ -729,7 +842,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 32. Multi-Runtime Support
|
||||
### 36. Multi-Runtime Support
|
||||
|
||||
**Purpose:** Run GSD across 6 different AI coding agent runtimes.
|
||||
|
||||
@@ -752,7 +865,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 33. Hook System
|
||||
### 37. Hook System
|
||||
|
||||
**Purpose:** Runtime event hooks for context monitoring, status display, and update checking.
|
||||
|
||||
@@ -772,7 +885,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
|
||||
|
||||
### 33. Developer Profiling
|
||||
### 38. Developer Profiling
|
||||
|
||||
**Command:** `/gsd:profile-user [--questionnaire] [--refresh]`
|
||||
|
||||
@@ -808,7 +921,7 @@ Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
|
||||
- REQ-PROF-03: Questionnaire MUST be available as fallback when no session history exists
|
||||
- REQ-PROF-04: Generated artifacts MUST be discoverable by Claude Code (CLAUDE.md integration)
|
||||
|
||||
### 34. Execution Hardening
|
||||
### 39. Execution Hardening
|
||||
|
||||
**Purpose:** Three additive quality improvements to the execution pipeline that catch cross-plan failures before they cascade.
|
||||
|
||||
@@ -827,3 +940,36 @@ After Level 3 wiring verification passes, spot-check individual exports for actu
|
||||
- REQ-HARD-01: Pre-wave check MUST verify key-links from all prior wave artifacts before spawning next wave
|
||||
- REQ-HARD-02: Cross-plan contract check MUST detect incompatible data transformations between plans
|
||||
- REQ-HARD-03: Export spot-check MUST identify dead stores in wired files
|
||||
|
||||
---
|
||||
|
||||
### 40. Verification Debt Tracking
|
||||
|
||||
**Command:** `/gsd:audit-uat`
|
||||
|
||||
**Purpose:** Prevent silent loss of UAT/verification items when projects advance past phases with outstanding tests. Surfaces verification debt across all prior phases so items are never forgotten.
|
||||
|
||||
**Components:**
|
||||
|
||||
**1. Cross-Phase Health Check** (progress.md Step 1.6)
|
||||
Every `/gsd:progress` call scans ALL phases in the current milestone for outstanding items (pending, skipped, blocked, human_needed). Displays a non-blocking warning section with actionable links.
|
||||
|
||||
**2. `status: partial`** (verify-work.md, UAT.md)
|
||||
New UAT status that distinguishes between "session ended" and "all tests resolved". Prevents `status: complete` when tests are still pending, blocked, or skipped without reason.
|
||||
|
||||
**3. `result: blocked` with `blocked_by` tag** (verify-work.md, UAT.md)
|
||||
New test result type for tests blocked by external dependencies (server, physical device, release build, third-party services). Categorized separately from skipped tests.
|
||||
|
||||
**4. HUMAN-UAT.md Persistence** (execute-phase.md)
|
||||
When verification returns `human_needed`, items are persisted as a trackable HUMAN-UAT.md file with `status: partial`. Feeds into the cross-phase health check and audit systems.
|
||||
|
||||
**5. Phase Completion Warnings** (phase.cjs, transition.md)
|
||||
`phase complete` CLI returns verification debt warnings in its JSON output. Transition workflow surfaces outstanding items before confirmation.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-DEBT-01: System MUST surface outstanding UAT/verification items from ALL prior phases in `/gsd:progress`
|
||||
- REQ-DEBT-02: System MUST distinguish incomplete testing (partial) from completed testing (complete)
|
||||
- REQ-DEBT-03: System MUST categorize blocked tests with `blocked_by` tags
|
||||
- REQ-DEBT-04: System MUST persist human_needed verification items as trackable UAT files
|
||||
- REQ-DEBT-05: System MUST warn (non-blocking) during phase completion and transition when verification debt exists
|
||||
- REQ-DEBT-06: `/gsd:audit-uat` MUST scan all phases, categorize items by testability, and produce a human test plan
|
||||
|
||||
@@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic
|
||||
│ │ /gsd:verify-work │ │ <- Manual UAT
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────┐ │
|
||||
│ │ /gsd:ship │ │ <- Create PR (optional)
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ Next Phase?────────────┘
|
||||
│ │ No
|
||||
└─────────────┼──────────────┘
|
||||
@@ -284,6 +288,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
|
||||
| `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase |
|
||||
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves | After planning is complete |
|
||||
| `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes |
|
||||
| `/gsd:ship [N]` | Create PR from verified work | After verification passes |
|
||||
| `/gsd:next` | Auto-detect state and run next step | Anytime — "what should I do next?" |
|
||||
| `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) |
|
||||
| `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone |
|
||||
| `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified |
|
||||
@@ -295,7 +301,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
|
||||
|---------|---------|-------------|
|
||||
| `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" |
|
||||
| `/gsd:resume-work` | Restore full context from last session | Starting a new session |
|
||||
| `/gsd:pause-work` | Save context handoff | Stopping mid-phase |
|
||||
| `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase |
|
||||
| `/gsd:session-report` | Generate session summary with work and outcomes | End of session, stakeholder sharing |
|
||||
| `/gsd:help` | Show all commands | Quick reference |
|
||||
| `/gsd:update` | Update GSD with changelog preview | Check for new versions |
|
||||
| `/gsd:join-discord` | Open Discord community invite | Questions or community |
|
||||
@@ -349,12 +356,13 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|
||||
"ui_phase": true,
|
||||
"ui_safety_gate": true
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}"
|
||||
}
|
||||
}
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
||||
"quick_branch_template": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Core Settings
|
||||
@@ -363,7 +371,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|
||||
|---------|---------|---------|------------------|
|
||||
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
|
||||
| `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity: how finely scope is sliced (3-5, 5-8, or 8-12 phases) |
|
||||
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
|
||||
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
|
||||
|
||||
### Planning Settings
|
||||
|
||||
@@ -391,9 +399,10 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
|
||||
|
||||
| Setting | Options | Default | What it Controls |
|
||||
|---------|---------|---------|------------------|
|
||||
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created |
|
||||
| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy |
|
||||
| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy |
|
||||
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created |
|
||||
| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy |
|
||||
| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy |
|
||||
| `git.quick_branch_template` | Template string or `null` | `null` | Optional branch name for `/gsd:quick` tasks |
|
||||
|
||||
**Branching strategies explained:**
|
||||
|
||||
@@ -403,29 +412,37 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
|
||||
| `phase` | At each `execute-phase` | One phase per branch | Code review per phase, granular rollback |
|
||||
| `milestone` | At first `execute-phase` | All phases share one branch | Release branches, PR per version |
|
||||
|
||||
**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0").
|
||||
**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0"), `{num}` / `{quick}` = quick task ID (e.g., "260317-abc").
|
||||
|
||||
Example quick-task branching:
|
||||
|
||||
```json
|
||||
"git": {
|
||||
"quick_branch_template": "gsd/quick-{num}-{slug}"
|
||||
}
|
||||
```
|
||||
|
||||
### Model Profiles (Per-Agent Breakdown)
|
||||
|
||||
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|
||||
|-------|-----------|------------|----------|-----------|
|
||||
| gsd-planner | Opus | Opus | Sonnet | Inherit |
|
||||
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
|
||||
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
|
||||
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
|
||||
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|
||||
|-------|-----------|------------|----------|-----------|
|
||||
| gsd-planner | Opus | Opus | Sonnet | Inherit |
|
||||
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
|
||||
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
|
||||
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
|
||||
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
|
||||
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
|
||||
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
|
||||
|
||||
**Profile philosophy:**
|
||||
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
|
||||
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
|
||||
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
|
||||
- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`).
|
||||
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
|
||||
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
|
||||
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
|
||||
- **inherit** -- All agents use the current session model. Best when switching models dynamically (e.g. OpenCode `/model`), or **required** when using non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs.
|
||||
|
||||
---
|
||||
|
||||
@@ -442,12 +459,14 @@ claude --dangerously-skip-permissions
|
||||
/gsd:plan-phase 1 # Research + plan + verify
|
||||
/gsd:execute-phase 1 # Parallel execution
|
||||
/gsd:verify-work 1 # Manual UAT
|
||||
/gsd:ship 1 # Create PR from verified work
|
||||
/gsd:ui-review 1 # Visual audit (frontend phases)
|
||||
/clear
|
||||
/gsd:discuss-phase 2 # Repeat for each phase
|
||||
/gsd:next # Auto-detect and run next step
|
||||
...
|
||||
/gsd:audit-milestone # Check everything shipped
|
||||
/gsd:complete-milestone # Archive, tag, done
|
||||
/gsd:session-report # Generate session summary
|
||||
```
|
||||
|
||||
### New Project from Existing Document
|
||||
@@ -539,6 +558,10 @@ Do not re-run `/gsd:execute-phase`. Use `/gsd:quick` for targeted fixes, or `/gs
|
||||
|
||||
Switch to budget profile: `/gsd:set-profile budget`. Disable research and plan-check agents via `/gsd:settings` if the domain is familiar to you (or to Claude).
|
||||
|
||||
### Using Non-Anthropic Models (OpenRouter, Local)
|
||||
|
||||
If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd:set-profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd:settings` → Model Profile → Inherit.
|
||||
|
||||
### Working on a Sensitive/Private Project
|
||||
|
||||
Set `commit_docs: false` during `/gsd:new-project` or via `/gsd:settings`. Add `.planning/` to your `.gitignore`. Planning artifacts stay local and never touch git.
|
||||
@@ -551,6 +574,21 @@ Since v1.17, the installer backs up locally modified files to `gsd-local-patches
|
||||
|
||||
A known workaround exists for a Claude Code classification bug. GSD's orchestrators (execute-phase, quick) spot-check actual output before reporting failure. If you see a failure message but commits were made, check `git log` -- the work may have succeeded.
|
||||
|
||||
### Parallel Execution Causes Build Lock Errors
|
||||
|
||||
If you see pre-commit hook failures, cargo lock contention, or 30+ minute execution times during parallel wave execution, this is caused by multiple agents triggering build tools simultaneously. GSD handles this automatically since v1.26 — parallel agents use `--no-verify` on commits and the orchestrator runs hooks once after each wave. If you're on an older version, add this to your project's `CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
## Git Commit Rules for Agents
|
||||
All subagent/executor commits MUST use `--no-verify`.
|
||||
```
|
||||
|
||||
To disable parallel execution entirely: `/gsd:settings` → set `parallelization.enabled` to `false`.
|
||||
|
||||
### Windows: Installation Crashes on Protected Directories
|
||||
|
||||
If the installer crashes with `EPERM: operation not permitted, scandir` on Windows, this is caused by OS-protected directories (e.g., Chromium browser profiles). Fixed since v1.24 — update to the latest version. As a workaround, temporarily rename the problematic directory before running the installer.
|
||||
|
||||
---
|
||||
|
||||
## Recovery Quick Reference
|
||||
@@ -566,6 +604,9 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat
|
||||
| Plan doesn't match your vision | `/gsd:discuss-phase [N]` then re-plan |
|
||||
| Costs running high | `/gsd:set-profile budget` and `/gsd:settings` to toggle agents off |
|
||||
| Update broke local changes | `/gsd:reapply-patches` |
|
||||
| Want session summary for stakeholder | `/gsd:session-report` |
|
||||
| Don't know what step is next | `/gsd:next` |
|
||||
| Parallel execution build errors | Update GSD or set `parallelization.enabled: false` |
|
||||
|
||||
---
|
||||
|
||||
@@ -581,7 +622,9 @@ For reference, here is what GSD creates in your project:
|
||||
STATE.md # Decisions, blockers, session memory
|
||||
config.json # Workflow configuration
|
||||
MILESTONES.md # Completed milestone archive
|
||||
HANDOFF.json # Structured session handoff (from /gsd:pause-work)
|
||||
research/ # Domain research from /gsd:new-project
|
||||
reports/ # Session reports (from /gsd:session-report)
|
||||
todos/
|
||||
pending/ # Captured ideas awaiting work
|
||||
done/ # Completed todos
|
||||
|
||||
707
docs/zh-CN/README.md
Normal file
707
docs/zh-CN/README.md
Normal file
@@ -0,0 +1,707 @@
|
||||
<div align="center">
|
||||
|
||||
# GET SHIT DONE
|
||||
|
||||
**一个轻量级且强大的元提示、上下文工程和规格驱动开发系统,支持 Claude Code、OpenCode、Gemini CLI 和 Codex。**
|
||||
|
||||
**解决上下文衰减 —— 即 Claude 填充上下文窗口时发生的质量退化问题。**
|
||||
|
||||
[](https://www.npmjs.com/package/get-shit-done-cc)
|
||||
[](https://www.npmjs.com/package/get-shit-done-cc)
|
||||
[](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml)
|
||||
[](https://discord.gg/gsd)
|
||||
[](https://x.com/gsd_foundation)
|
||||
[](https://dexscreener.com/solana/dwudwjvan7bzkw9zwlbyv6kspdlvhwzrqy6ebk8xzxkv)
|
||||
[](https://github.com/glittercowboy/get-shit-done)
|
||||
[](LICENSE)
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
**支持 Mac、Windows 和 Linux。**
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
*"如果你清楚自己想要什么,它真的会帮你构建出来。不忽悠。"*
|
||||
|
||||
*"我试过 SpecKit、OpenSpec 和 Taskmaster —— 这是我用过的效果最好的。"*
|
||||
|
||||
*"这是我用过的 Claude Code 最强大的扩展。没有过度设计。真的就是把事情做完。"*
|
||||
|
||||
<br>
|
||||
|
||||
**被 Amazon、Google、Shopify 和 Webflow 的工程师信赖使用。**
|
||||
|
||||
[我为什么开发这个](#我为什么开发这个) · [工作原理](#工作原理) · [命令](#命令) · [为什么有效](#为什么有效) · [用户指南](USER-GUIDE.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 我为什么开发这个
|
||||
|
||||
我是一名独立开发者。我不写代码 —— Claude Code 写。
|
||||
|
||||
其他规格驱动开发工具确实存在,比如 BMAD、Speckit... 但它们似乎都把事情搞得比实际需要的复杂得多(冲刺会议、故事点、干系人同步、回顾、Jira 工作流),或者缺乏对你正在构建的东西的真正大局理解。我不是一个 50 人的软件公司。我不想搞企业级表演。我只是个想构建出好用的东西的创意人。
|
||||
|
||||
所以我开发了 GSD。复杂性在系统内部,不在你的工作流里。幕后是:上下文工程、XML 提示格式、子代理编排、状态管理。你看到的是:几个命令,用就完了。
|
||||
|
||||
系统给 Claude 提供了它完成工作**以及**验证工作所需的一切。我信任这个工作流。它就是做得好。
|
||||
|
||||
这就是它的本质。没有企业级角色扮演的废话。只是一个让 Claude Code 稳定可靠地构建酷东西的极其有效的系统。
|
||||
|
||||
— **TÂCHES**
|
||||
|
||||
---
|
||||
|
||||
Vibecoding 名声不好。你描述想要什么,AI 生成代码,结果得到不一致的垃圾,规模一大就崩。
|
||||
|
||||
GSD 解决了这个问题。它是让 Claude Code 变得可靠的上下文工程层。描述你的想法,让系统提取它需要知道的一切,然后让 Claude Code 开始工作。
|
||||
|
||||
---
|
||||
|
||||
## 这个工具适合谁
|
||||
|
||||
想要描述需求然后正确构建出来的人 —— 不用假装自己在运营一个 50 人的工程组织。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
安装程序会提示你选择:
|
||||
1. **运行时** —— Claude Code、OpenCode、Gemini、Codex 或全部
|
||||
2. **位置** —— 全局(所有项目)或本地(仅当前项目)
|
||||
|
||||
验证安装:
|
||||
- Claude Code / Gemini: `/gsd:help`
|
||||
- OpenCode: `/gsd-help`
|
||||
- Codex: `$gsd-help`
|
||||
|
||||
> [!NOTE]
|
||||
> Codex 安装使用技能(`skills/gsd-*/SKILL.md`)而非自定义提示。
|
||||
|
||||
### 保持更新
|
||||
|
||||
GSD 快速迭代。定期更新:
|
||||
|
||||
```bash
|
||||
npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>非交互式安装(Docker、CI、脚本)</strong></summary>
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
npx get-shit-done-cc --claude --global # 安装到 ~/.claude/
|
||||
npx get-shit-done-cc --claude --local # 安装到 ./.claude/
|
||||
|
||||
# OpenCode(开源,免费模型)
|
||||
npx get-shit-done-cc --opencode --global # 安装到 ~/.config/opencode/
|
||||
|
||||
# Gemini CLI
|
||||
npx get-shit-done-cc --gemini --global # 安装到 ~/.gemini/
|
||||
|
||||
# Codex(技能优先)
|
||||
npx get-shit-done-cc --codex --global # 安装到 ~/.codex/
|
||||
npx get-shit-done-cc --codex --local # 安装到 ./.codex/
|
||||
|
||||
# 所有运行时
|
||||
npx get-shit-done-cc --all --global # 安装到所有目录
|
||||
```
|
||||
|
||||
使用 `--global`(`-g`)或 `--local`(`-l`)跳过位置提示。
|
||||
使用 `--claude`、`--opencode`、`--gemini`、`--codex` 或 `--all` 跳过运行时提示。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>开发安装</strong></summary>
|
||||
|
||||
克隆仓库并本地运行安装程序:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/glittercowboy/get-shit-done.git
|
||||
cd get-shit-done
|
||||
node bin/install.js --claude --local
|
||||
```
|
||||
|
||||
安装到 `./.claude/` 用于在贡献前测试修改。
|
||||
|
||||
</details>
|
||||
|
||||
### 推荐:跳过权限模式
|
||||
|
||||
GSD 设计为无摩擦自动化。运行 Claude Code 时使用:
|
||||
|
||||
```bash
|
||||
claude --dangerously-skip-permissions
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 这是 GSD 的预期使用方式 —— 停下来 50 次批准 `date` 和 `git commit` 会失去意义。
|
||||
|
||||
<details>
|
||||
<summary><strong>替代方案:细粒度权限</strong></summary>
|
||||
|
||||
如果你不想使用那个标志,在项目的 `.claude/settings.json` 中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(date:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(tr:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git tag:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
> **已有代码?** 先运行 `/gsd:map-codebase`。它会生成并行代理分析你的技术栈、架构、约定和关注点。然后 `/gsd:new-project` 就了解你的代码库了 —— 问题聚焦在你正在**添加**什么,规划会自动加载你的模式。
|
||||
|
||||
### 1. 初始化项目
|
||||
|
||||
```
|
||||
/gsd:new-project
|
||||
```
|
||||
|
||||
一条命令,一个流程。系统:
|
||||
|
||||
1. **提问** —— 问到完全理解你的想法为止(目标、约束、技术偏好、边缘情况)
|
||||
2. **研究** —— 生成并行代理调查领域(可选但推荐)
|
||||
3. **需求** —— 提取哪些是 v1、v2 和范围外
|
||||
4. **路线图** —— 创建映射到需求的阶段
|
||||
|
||||
你批准路线图。现在准备好构建了。
|
||||
|
||||
**创建:** `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md`、`.planning/research/`
|
||||
|
||||
---
|
||||
|
||||
### 2. 讨论阶段
|
||||
|
||||
```
|
||||
/gsd:discuss-phase 1
|
||||
```
|
||||
|
||||
**这是你塑造实现方式的地方。**
|
||||
|
||||
你的路线图每个阶段有一两句话。这不足以按照**你**想象的方式构建东西。这一步在研究或规划之前捕获你的偏好。
|
||||
|
||||
系统分析阶段并根据正在构建的内容识别灰色区域:
|
||||
|
||||
- **视觉功能** → 布局、密度、交互、空状态
|
||||
- **API/CLI** → 响应格式、标志、错误处理、详细程度
|
||||
- **内容系统** → 结构、语气、深度、流程
|
||||
- **组织任务** → 分组标准、命名、重复项、例外
|
||||
|
||||
对于你选择的每个领域,它会问到让你满意为止。输出 —— `CONTEXT.md` —— 直接输入接下来的两个步骤:
|
||||
|
||||
1. **研究员读取它** —— 知道要调查什么模式("用户想要卡片布局" → 研究卡片组件库)
|
||||
2. **规划者读取它** —— 知道哪些决策已锁定("无限滚动已决定" → 规划包含滚动处理)
|
||||
|
||||
你在这里走得越深,系统构建的就越是你真正想要的。跳过它你会得到合理的默认值。使用它你会得到**你的**愿景。
|
||||
|
||||
**创建:** `{阶段号}-CONTEXT.md`
|
||||
|
||||
---
|
||||
|
||||
### 3. 规划阶段
|
||||
|
||||
```
|
||||
/gsd:plan-phase 1
|
||||
```
|
||||
|
||||
系统:
|
||||
|
||||
1. **研究** —— 调查如何实现这个阶段,由你的 CONTEXT.md 决策指导
|
||||
2. **规划** —— 创建 2-3 个带有 XML 结构的原子任务计划
|
||||
3. **验证** —— 根据需求检查计划,循环直到通过
|
||||
|
||||
每个计划足够小,可以在全新的上下文窗口中执行。没有退化,没有"我现在会更简洁"。
|
||||
|
||||
**创建:** `{阶段号}-RESEARCH.md`、`{阶段号}-{N}-PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
### 4. 执行阶段
|
||||
|
||||
```
|
||||
/gsd:execute-phase 1
|
||||
```
|
||||
|
||||
系统:
|
||||
|
||||
1. **按波次运行计划** —— 可能的话并行,有依赖时顺序
|
||||
2. **每个计划全新上下文** —— 200k token 纯粹用于实现,零累积垃圾
|
||||
3. **每个任务提交** —— 每个任务都有自己的原子提交
|
||||
4. **根据目标验证** —— 检查代码库是否交付了阶段承诺的内容
|
||||
|
||||
离开,回来看到完成的工作和干净的 git 历史。
|
||||
|
||||
**波次执行工作原理:**
|
||||
|
||||
计划根据依赖关系分组到"波次"。在每个波次内,计划并行运行。波次顺序执行。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段执行 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 波次 1 (并行) 波次 2 (并行) 波次 3 │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 计划 01 │ │ 计划 02 │ → │ 计划 03 │ │ 计划 04 │ → │ 计划 05 │ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │
|
||||
│ │ 用户 │ │ 产品 │ │ 订单 │ │ 购物车 │ │ 结账 │ │
|
||||
│ │ 模型 │ │ 模型 │ │ API │ │ API │ │ UI │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │ │ ↑ ↑ ↑ │
|
||||
│ └───────────┴──────────────┴───────────┘ │ │
|
||||
│ 依赖关系: 计划 03 需要计划 01 │ │
|
||||
│ 计划 04 需要计划 02 │ │
|
||||
│ 计划 05 需要计划 03 + 04 │ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**为什么波次重要:**
|
||||
- 独立计划 → 同一波次 → 并行运行
|
||||
- 依赖计划 → 后续波次 → 等待依赖
|
||||
- 文件冲突 → 顺序计划或同一计划
|
||||
|
||||
这就是为什么"垂直切片"(计划 01: 用户功能端到端)比"水平分层"(计划 01: 所有模型,计划 02: 所有 API)并行化更好。
|
||||
|
||||
**创建:** `{阶段号}-{N}-SUMMARY.md`、`{阶段号}-VERIFICATION.md`
|
||||
|
||||
---
|
||||
|
||||
### 5. 验证工作
|
||||
|
||||
```
|
||||
/gsd:verify-work 1
|
||||
```
|
||||
|
||||
**这是你确认它真的有效的地方。**
|
||||
|
||||
自动化验证检查代码存在和测试通过。但功能是否按你预期的方式**工作**?这是你使用它的机会。
|
||||
|
||||
系统:
|
||||
|
||||
1. **提取可测试交付物** —— 你现在应该能做什么
|
||||
2. **逐个引导你** —— "你能用邮箱登录吗?" 是/否,或描述有什么问题
|
||||
3. **自动诊断失败** —— 生成调试代理找根本原因
|
||||
4. **创建已验证的修复计划** —— 准备立即重新执行
|
||||
|
||||
如果一切通过,继续。如果有东西坏了,不用手动调试 —— 只需再次运行 `/gsd:execute-phase`,使用它创建的修复计划。
|
||||
|
||||
**创建:** `{阶段号}-UAT.md`,如果发现问题则创建修复计划
|
||||
|
||||
---
|
||||
|
||||
### 6. 循环 → 完成 → 下一个里程碑
|
||||
|
||||
```
|
||||
/gsd:discuss-phase 2
|
||||
/gsd:plan-phase 2
|
||||
/gsd:execute-phase 2
|
||||
/gsd:verify-work 2
|
||||
...
|
||||
/gsd:complete-milestone
|
||||
/gsd:new-milestone
|
||||
```
|
||||
|
||||
循环 **讨论 → 规划 → 执行 → 验证** 直到里程碑完成。
|
||||
|
||||
如果你想在讨论期间更快速地输入,使用 `/gsd:discuss-phase <n> --batch` 一次回答一组小问题,而不是一个一个来。
|
||||
|
||||
每个阶段都会获得你的输入(讨论)、适当的研究(规划)、干净的执行(执行)和人工验证(验证)。上下文保持新鲜。质量保持高水平。
|
||||
|
||||
当所有阶段完成后,`/gsd:complete-milestone` 归档里程碑并标记发布。
|
||||
|
||||
然后 `/gsd:new-milestone` 开始下一个版本 —— 与 `new-project` 相同的流程,但针对你现有的代码库。你描述接下来想构建什么,系统研究领域,你界定需求范围,它创建新的路线图。每个里程碑是一个干净的周期:定义 → 构建 → 发布。
|
||||
|
||||
---
|
||||
|
||||
### 快速模式
|
||||
|
||||
```
|
||||
/gsd:quick
|
||||
```
|
||||
|
||||
**用于不需要完整规划的临时任务。**
|
||||
|
||||
快速模式给你 GSD 保证(原子提交、状态跟踪)和更快的路径:
|
||||
|
||||
- **相同代理** —— 规划者 + 执行者,相同质量
|
||||
- **跳过可选步骤** —— 无研究、无计划检查器、无验证器
|
||||
- **独立跟踪** —— 存放在 `.planning/quick/`,不是阶段
|
||||
|
||||
用于:bug 修复、小功能、配置更改、一次性任务。
|
||||
|
||||
```
|
||||
/gsd:quick
|
||||
> 你想做什么?"在设置中添加深色模式切换"
|
||||
```
|
||||
|
||||
**创建:** `.planning/quick/001-add-dark-mode-toggle/PLAN.md`、`SUMMARY.md`
|
||||
|
||||
---
|
||||
|
||||
## 为什么有效
|
||||
|
||||
### 上下文工程
|
||||
|
||||
Claude Code 非常强大,**如果你**给它需要的上下文。大多数人没有。
|
||||
|
||||
GSD 为你处理:
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `PROJECT.md` | 项目愿景,始终加载 |
|
||||
| `research/` | 生态知识(技术栈、功能、架构、陷阱) |
|
||||
| `REQUIREMENTS.md` | 界定 v1/v2 需求及阶段可追溯性 |
|
||||
| `ROADMAP.md` | 你要去哪里,完成了什么 |
|
||||
| `STATE.md` | 决策、阻塞项、位置 —— 跨会话记忆 |
|
||||
| `PLAN.md` | 带有 XML 结构和验证步骤的原子任务 |
|
||||
| `SUMMARY.md` | 发生了什么,改了什么,提交到历史 |
|
||||
| `todos/` | 为后续工作捕获的想法和任务 |
|
||||
|
||||
基于 Claude 质量退化的位置设置大小限制。保持在限制内,获得一致的卓越。
|
||||
|
||||
### XML 提示格式
|
||||
|
||||
每个计划都是为 Claude 优化的结构化 XML:
|
||||
|
||||
```xml
|
||||
<task type="auto">
|
||||
<name>创建登录端点</name>
|
||||
<files>src/app/api/auth/login/route.ts</files>
|
||||
<action>
|
||||
使用 jose 处理 JWT(不用 jsonwebtoken - CommonJS 问题)。
|
||||
根据 users 表验证凭据。
|
||||
成功时返回 httpOnly cookie。
|
||||
</action>
|
||||
<verify>curl -X POST localhost:3000/api/auth/login 返回 200 + Set-Cookie</verify>
|
||||
<done>有效凭据返回 cookie,无效返回 401</done>
|
||||
</task>
|
||||
```
|
||||
|
||||
精确的指令。不猜测。内置验证。
|
||||
|
||||
### 多代理编排
|
||||
|
||||
每个阶段使用相同模式:轻量编排器生成专门代理,收集结果,路由到下一步。
|
||||
|
||||
| 阶段 | 编排器做 | 代理做 |
|
||||
|-------|------------------|-----------|
|
||||
| 研究 | 协调,呈现发现 | 4 个并行研究员调查技术栈、功能、架构、陷阱 |
|
||||
| 规划 | 验证,管理迭代 | 规划者创建计划,检查器验证,循环直到通过 |
|
||||
| 执行 | 分组为波次,跟踪进度 | 执行者并行实现,每个有全新 200k 上下文 |
|
||||
| 验证 | 呈现结果,路由下一步 | 验证器根据目标检查代码库,调试器诊断失败 |
|
||||
|
||||
编排器从不做重活。它生成代理,等待,整合结果。
|
||||
|
||||
**结果:** 你可以运行整个阶段 —— 深度研究、多个计划创建和验证、跨并行执行者编写数千行代码、根据目标自动化验证 —— 你的主上下文窗口保持在 30-40%。工作在全新的子代理上下文中完成。你的会话保持快速和响应。
|
||||
|
||||
### 原子 Git 提交
|
||||
|
||||
每个任务在完成后立即获得自己的提交:
|
||||
|
||||
```bash
|
||||
abc123f docs(08-02): 完成用户注册计划
|
||||
def456g feat(08-02): 添加邮箱确认流程
|
||||
hij789k feat(08-02): 实现密码哈希
|
||||
lmn012o feat(08-02): 创建注册端点
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **好处:** Git bisect 找到确切的失败任务。每个任务独立可回滚。未来会话中 Claude 的清晰历史。AI 自动化工作流中更好的可观察性。
|
||||
|
||||
每个提交都是精确的、可追溯的、有意义的。
|
||||
|
||||
### 模块化设计
|
||||
|
||||
- 向当前里程碑添加阶段
|
||||
- 在阶段之间插入紧急工作
|
||||
- 完成里程碑并重新开始
|
||||
- 调整计划而不重建一切
|
||||
|
||||
你永远不会被锁定。系统会适应。
|
||||
|
||||
---
|
||||
|
||||
## 命令
|
||||
|
||||
### 核心工作流
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:new-project [--auto]` | 完整初始化:提问 → 研究 → 需求 → 路线图 |
|
||||
| `/gsd:discuss-phase [N] [--auto]` | 在规划前捕获实现决策 |
|
||||
| `/gsd:plan-phase [N] [--auto]` | 阶段的研究 + 规划 + 验证 |
|
||||
| `/gsd:execute-phase <N>` | 在并行波次中执行所有计划,完成后验证 |
|
||||
| `/gsd:verify-work [N]` | 手动用户验收测试 ¹ |
|
||||
| `/gsd:audit-milestone` | 验证里程碑达到了其完成定义 |
|
||||
| `/gsd:complete-milestone` | 归档里程碑,标记发布 |
|
||||
| `/gsd:new-milestone [name]` | 开始下一个版本:提问 → 研究 → 需求 → 路线图 |
|
||||
|
||||
### 导航
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:progress` | 我在哪?接下来做什么? |
|
||||
| `/gsd:help` | 显示所有命令和使用指南 |
|
||||
| `/gsd:update` | 更新 GSD 并预览变更日志 |
|
||||
| `/gsd:join-discord` | 加入 GSD Discord 社区 |
|
||||
|
||||
### 现有代码库
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:map-codebase` | 在 new-project 之前分析现有代码库 |
|
||||
|
||||
### 阶段管理
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:add-phase` | 向路线图追加阶段 |
|
||||
| `/gsd:insert-phase [N]` | 在阶段之间插入紧急工作 |
|
||||
| `/gsd:remove-phase [N]` | 删除未来阶段,重新编号 |
|
||||
| `/gsd:list-phase-assumptions [N]` | 规划前查看 Claude 的预期方法 |
|
||||
| `/gsd:plan-milestone-gaps` | 创建阶段以填补审计发现的差距 |
|
||||
|
||||
### 会话
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:pause-work` | 阶段中途停止时创建交接 |
|
||||
| `/gsd:resume-work` | 从上次会话恢复 |
|
||||
|
||||
### 工具
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd:settings` | 配置模型配置文件和工作流代理 |
|
||||
| `/gsd:set-profile <profile>` | 切换模型配置文件(quality/balanced/budget) |
|
||||
| `/gsd:add-todo [desc]` | 捕获想法留待后用 |
|
||||
| `/gsd:check-todos` | 列出待处理事项 |
|
||||
| `/gsd:debug [desc]` | 带持久状态的系统化调试 |
|
||||
| `/gsd:quick [--full] [--discuss]` | 用 GSD 保证执行临时任务(`--full` 添加计划检查和验证,`--discuss` 先收集上下文) |
|
||||
| `/gsd:health [--repair]` | 验证 `.planning/` 目录完整性,用 `--repair` 自动修复 |
|
||||
|
||||
<sup>¹ 由 Reddit 用户 OracleGreyBeard 贡献</sup>
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。完整配置模式、工作流开关、git 分支选项和每个代理的模型分解,请参阅[用户指南](USER-GUIDE.md#配置参考)。
|
||||
|
||||
### 核心设置
|
||||
|
||||
| 设置 | 选项 | 默认值 | 控制内容 |
|
||||
|---------|---------|---------|------------------|
|
||||
| `mode` | `yolo`, `interactive` | `interactive` | 自动批准 vs 每步确认 |
|
||||
| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度 —— 范围切分多细(阶段 × 计划) |
|
||||
|
||||
### 模型配置
|
||||
|
||||
控制每个代理使用哪个 Claude 模型。平衡质量和 token 消耗。
|
||||
|
||||
| 配置 | 规划 | 执行 | 验证 |
|
||||
|---------|----------|-----------|--------------|
|
||||
| `quality` | Opus | Opus | Sonnet |
|
||||
| `balanced`(默认) | Opus | Sonnet | Sonnet |
|
||||
| `budget` | Sonnet | Sonnet | Haiku |
|
||||
|
||||
切换配置:
|
||||
```
|
||||
/gsd:set-profile budget
|
||||
```
|
||||
|
||||
或通过 `/gsd:settings` 配置。
|
||||
|
||||
### 工作流代理
|
||||
|
||||
这些在规划/执行期间生成额外代理。它们提高质量但增加 token 和时间。
|
||||
|
||||
| 设置 | 默认值 | 作用 |
|
||||
|---------|---------|--------------|
|
||||
| `workflow.research` | `true` | 每个阶段规划前研究领域 |
|
||||
| `workflow.plan_check` | `true` | 执行前验证计划是否达到阶段目标 |
|
||||
| `workflow.verifier` | `true` | 执行后确认必须项已交付 |
|
||||
| `workflow.auto_advance` | `false` | 自动链式执行 讨论 → 规划 → 执行 |
|
||||
|
||||
使用 `/gsd:settings` 切换这些,或每次调用时覆盖:
|
||||
- `/gsd:plan-phase --skip-research`
|
||||
- `/gsd:plan-phase --skip-verify`
|
||||
|
||||
### 执行
|
||||
|
||||
| 设置 | 默认值 | 控制内容 |
|
||||
|---------|---------|------------------|
|
||||
| `parallelization.enabled` | `true` | 同时运行独立计划 |
|
||||
| `planning.commit_docs` | `true` | 在 git 中跟踪 `.planning/` |
|
||||
|
||||
### Git 分支
|
||||
|
||||
控制 GSD 在执行期间如何处理分支。
|
||||
|
||||
| 设置 | 选项 | 默认值 | 作用 |
|
||||
|---------|---------|---------|--------------|
|
||||
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 分支创建策略 |
|
||||
| `git.phase_branch_template` | 字符串 | `gsd/phase-{phase}-{slug}` | 阶段分支模板 |
|
||||
| `git.milestone_branch_template` | 字符串 | `gsd/{milestone}-{slug}` | 里程碑分支模板 |
|
||||
|
||||
**策略:**
|
||||
- **`none`** —— 提交到当前分支(默认 GSD 行为)
|
||||
- **`phase`** —— 每个阶段创建一个分支,阶段完成时合并
|
||||
- **`milestone`** —— 为整个里程碑创建一个分支,完成时合并
|
||||
|
||||
在里程碑完成时,GSD 提供 squash 合并(推荐)或带历史合并。
|
||||
|
||||
---
|
||||
|
||||
## 安全
|
||||
|
||||
### 保护敏感文件
|
||||
|
||||
GSD 的代码库映射和分析命令读取文件以了解你的项目。**保护包含密钥的文件**,将它们添加到 Claude Code 的拒绝列表:
|
||||
|
||||
1. 打开 Claude Code 设置(`.claude/settings.json` 或全局)
|
||||
2. 将敏感文件模式添加到拒绝列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(.env)",
|
||||
"Read(.env.*)",
|
||||
"Read(**/secrets/*)",
|
||||
"Read(**/*credential*)",
|
||||
"Read(**/*.pem)",
|
||||
"Read(**/*.key)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这完全阻止 Claude 读取这些文件,无论你运行什么命令。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> GSD 包含内置保护以防止提交密钥,但纵深防御是最佳实践。拒绝读取敏感文件作为第一道防线。
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
**安装后找不到命令?**
|
||||
- 重启运行时以重新加载命令/技能
|
||||
- 验证文件是否存在于 `~/.claude/commands/gsd/`(全局)或 `./.claude/commands/gsd/`(本地)
|
||||
- 对于 Codex,验证技能是否存在于 `~/.codex/skills/gsd-*/SKILL.md`(全局)或 `./.codex/skills/gsd-*/SKILL.md`(本地)
|
||||
|
||||
**命令没有按预期工作?**
|
||||
- 运行 `/gsd:help` 验证安装
|
||||
- 重新运行 `npx get-shit-done-cc` 重新安装
|
||||
|
||||
**更新到最新版本?**
|
||||
```bash
|
||||
npx get-shit-done-cc@latest
|
||||
```
|
||||
|
||||
**使用 Docker 或容器化环境?**
|
||||
|
||||
如果用波浪号路径(`~/.claude/...`)读取文件失败,在安装前设置 `CLAUDE_CONFIG_DIR`:
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global
|
||||
```
|
||||
这确保使用绝对路径而不是 `~`,后者在容器中可能无法正确展开。
|
||||
|
||||
### 卸载
|
||||
|
||||
完全删除 GSD:
|
||||
|
||||
```bash
|
||||
# 全局安装
|
||||
npx get-shit-done-cc --claude --global --uninstall
|
||||
npx get-shit-done-cc --opencode --global --uninstall
|
||||
npx get-shit-done-cc --codex --global --uninstall
|
||||
|
||||
# 本地安装(当前项目)
|
||||
npx get-shit-done-cc --claude --local --uninstall
|
||||
npx get-shit-done-cc --opencode --local --uninstall
|
||||
npx get-shit-done-cc --codex --local --uninstall
|
||||
```
|
||||
|
||||
这删除所有 GSD 命令、代理、钩子和设置,同时保留你的其他配置。
|
||||
|
||||
---
|
||||
|
||||
## 社区移植
|
||||
|
||||
OpenCode、Gemini CLI 和 Codex 现在通过 `npx get-shit-done-cc` 原生支持。
|
||||
|
||||
这些社区移植开创了多运行时支持:
|
||||
|
||||
| 项目 | 平台 | 描述 |
|
||||
|---------|----------|-------------|
|
||||
| [gsd-opencode](https://github.com/rokicool/gsd-opencode) | OpenCode | 原始 OpenCode 适配 |
|
||||
| gsd-gemini (已归档) | Gemini CLI | 由 uberfuzzy 开发的原始 Gemini 适配 |
|
||||
|
||||
---
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#glittercowboy/get-shit-done&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=glittercowboy/get-shit-done&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT 许可证。详见 [LICENSE](../LICENSE)。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Claude Code 很强大。GSD 让它可靠。**
|
||||
|
||||
</div>
|
||||
492
docs/zh-CN/USER-GUIDE.md
Normal file
492
docs/zh-CN/USER-GUIDE.md
Normal file
@@ -0,0 +1,492 @@
|
||||
# GSD 用户指南
|
||||
|
||||
工作流、故障排除和配置的详细参考。快速入门设置请参阅 [README](README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [工作流图解](#工作流图解)
|
||||
- [命令参考](#命令参考)
|
||||
- [配置参考](#配置参考)
|
||||
- [使用示例](#使用示例)
|
||||
- [故障排除](#故障排除)
|
||||
- [恢复快速参考](#恢复快速参考)
|
||||
|
||||
---
|
||||
|
||||
## 工作流图解
|
||||
|
||||
### 完整项目生命周期
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 新建项目 │
|
||||
│ /gsd:new-project │
|
||||
│ 提问 -> 研究 -> 需求 -> 路线图 │
|
||||
└─────────────────────────┬────────────────────────┘
|
||||
│
|
||||
┌──────────────▼─────────────┐
|
||||
│ 每个阶段: │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ /gsd:discuss-phase │ │ <- 锁定偏好
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────┐ │
|
||||
│ │ /gsd:plan-phase │ │ <- 研究 + 规划 + 验证
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────┐ │
|
||||
│ │ /gsd:execute-phase │ │ <- 并行执行
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────┐ │
|
||||
│ │ /gsd:verify-work │ │ <- 手动 UAT
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ 下一阶段?────────────┘
|
||||
│ │ 否
|
||||
└─────────────┼──────────────┘
|
||||
│
|
||||
┌───────────────▼──────────────┐
|
||||
│ /gsd:audit-milestone │
|
||||
│ /gsd:complete-milestone │
|
||||
└───────────────┬──────────────┘
|
||||
│
|
||||
另一个里程碑?
|
||||
│ │
|
||||
是 否 -> 完成!
|
||||
│
|
||||
┌───────▼──────────────┐
|
||||
│ /gsd:new-milestone │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 规划代理协调
|
||||
|
||||
```
|
||||
/gsd:plan-phase N
|
||||
│
|
||||
├── 阶段研究员 (x4 并行)
|
||||
│ ├── 技术栈研究员
|
||||
│ ├── 功能研究员
|
||||
│ ├── 架构研究员
|
||||
│ └── 陷阱研究员
|
||||
│ │
|
||||
│ ┌──────▼──────┐
|
||||
│ │ RESEARCH.md │
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌──────▼──────┐
|
||||
│ │ 规划者 │ <- 读取 PROJECT.md, REQUIREMENTS.md,
|
||||
│ │ │ CONTEXT.md, RESEARCH.md
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌──────▼───────────┐ ┌────────┐
|
||||
│ │ 计划检查器 │────>│ 通过? │
|
||||
│ └──────────────────┘ └───┬────┘
|
||||
│ │
|
||||
│ 是 │ 否
|
||||
│ │ │ │
|
||||
│ │ └───┘ (循环,最多 3 次)
|
||||
│ │
|
||||
│ ┌─────▼──────┐
|
||||
│ │ PLAN 文件 │
|
||||
│ └────────────┘
|
||||
└── 完成
|
||||
```
|
||||
|
||||
### 验证架构 (Nyquist 层)
|
||||
|
||||
在 plan-phase 研究期间,GSD 现在在任何代码编写之前将自动化测试覆盖率映射到每个阶段需求。这确保当 Claude 的执行者提交任务时,反馈机制已经存在可以在几秒钟内验证它。
|
||||
|
||||
研究员检测你现有的测试基础设施,将每个需求映射到特定的测试命令,并识别在实现开始之前必须创建的任何测试脚手架(波次 0 任务)。
|
||||
|
||||
计划检查器将其强制作为第 8 个验证维度:缺少自动化验证命令的计划将不会被批准。
|
||||
|
||||
**输出:** `{阶段}-VALIDATION.md` —— 阶段的反馈契约。
|
||||
|
||||
**禁用:** 在 `/gsd:settings` 中设置 `workflow.nyquist_validation: false`,用于测试基础设施不是重点的快速原型阶段。
|
||||
|
||||
### 追溯验证 (`/gsd:validate-phase`)
|
||||
|
||||
对于在 Nyquist 验证存在之前执行的阶段,或只有传统测试套件的现有代码库,追溯审计并填补覆盖缺口:
|
||||
|
||||
```
|
||||
/gsd:validate-phase N
|
||||
|
|
||||
+-- 检测状态 (VALIDATION.md 存在? SUMMARY.md 存在?)
|
||||
|
|
||||
+-- 发现: 扫描实现,将需求映射到测试
|
||||
|
|
||||
+-- 分析缺口: 哪些需求缺少自动化验证?
|
||||
|
|
||||
+-- 呈现缺口计划供审批
|
||||
|
|
||||
+-- 生成审计器: 生成测试,运行,调试(最多 3 次尝试)
|
||||
|
|
||||
+-- 更新 VALIDATION.md
|
||||
|
|
||||
+-- COMPLIANT -> 所有需求都有自动化检查
|
||||
+-- PARTIAL -> 部分缺口升级为仅手动
|
||||
```
|
||||
|
||||
审计器从不修改实现代码 —— 只修改测试文件和 VALIDATION.md。如果测试发现实现 bug,它会标记为升级让你处理。
|
||||
|
||||
**何时使用:** 在启用了 Nyquist 之前规划的阶段执行后,或在 `/gsd:audit-milestone` 发现 Nyquist 合规缺口后。
|
||||
|
||||
### 执行波次协调
|
||||
|
||||
```
|
||||
/gsd:execute-phase N
|
||||
│
|
||||
├── 分析计划依赖
|
||||
│
|
||||
├── 波次 1 (独立计划):
|
||||
│ ├── 执行者 A (全新 200K 上下文) -> 提交
|
||||
│ └── 执行者 B (全新 200K 上下文) -> 提交
|
||||
│
|
||||
├── 波次 2 (依赖波次 1):
|
||||
│ └── 执行者 C (全新 200K 上下文) -> 提交
|
||||
│
|
||||
└── 验证器
|
||||
└── 根据阶段目标检查代码库
|
||||
│
|
||||
├── 通过 -> VERIFICATION.md (成功)
|
||||
└── 失败 -> 问题记录到 /gsd:verify-work
|
||||
```
|
||||
|
||||
### 现有代码库工作流
|
||||
|
||||
```
|
||||
/gsd:map-codebase
|
||||
│
|
||||
├── 技术栈映射器 -> codebase/STACK.md
|
||||
├── 架构映射器 -> codebase/ARCHITECTURE.md
|
||||
├── 约定映射器 -> codebase/CONVENTIONS.md
|
||||
└── 关注点映射器 -> codebase/CONCERNS.md
|
||||
│
|
||||
┌───────▼──────────┐
|
||||
│ /gsd:new-project │ <- 问题聚焦于你正在添加的内容
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 命令参考
|
||||
|
||||
### 核心工作流
|
||||
|
||||
| 命令 | 用途 | 何时使用 |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd:new-project` | 完整项目初始化:提问、研究、需求、路线图 | 新项目开始时 |
|
||||
| `/gsd:new-project --auto @idea.md` | 从文档自动初始化 | 有现成的 PRD 或想法文档 |
|
||||
| `/gsd:discuss-phase [N]` | 捕获实现决策 | 规划前,塑造构建方式 |
|
||||
| `/gsd:plan-phase [N]` | 研究 + 规划 + 验证 | 执行阶段前 |
|
||||
| `/gsd:execute-phase <N>` | 在并行波次中执行所有计划 | 规划完成后 |
|
||||
| `/gsd:verify-work [N]` | 带自动诊断的手动 UAT | 执行完成后 |
|
||||
| `/gsd:audit-milestone` | 验证里程碑达到其完成定义 | 完成里程碑前 |
|
||||
| `/gsd:complete-milestone` | 归档里程碑,标记发布 | 所有阶段已验证 |
|
||||
| `/gsd:new-milestone [name]` | 开始下一个版本周期 | 完成里程碑后 |
|
||||
|
||||
### 导航
|
||||
|
||||
| 命令 | 用途 | 何时使用 |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd:progress` | 显示状态和下一步 | 任何时候 -- "我在哪?" |
|
||||
| `/gsd:resume-work` | 从上次会话恢复完整上下文 | 开始新会话 |
|
||||
| `/gsd:pause-work` | 保存上下文交接 | 阶段中途停止 |
|
||||
| `/gsd:help` | 显示所有命令 | 快速参考 |
|
||||
| `/gsd:update` | 更新 GSD 并预览变更日志 | 检查新版本 |
|
||||
| `/gsd:join-discord` | 打开 Discord 社区邀请 | 问题或社区 |
|
||||
|
||||
### 阶段管理
|
||||
|
||||
| 命令 | 用途 | 何时使用 |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd:add-phase` | 向路线图追加新阶段 | 初始规划后范围增长 |
|
||||
| `/gsd:insert-phase [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 |
|
||||
| `/gsd:remove-phase [N]` | 删除未来阶段并重新编号 | 移除某个功能 |
|
||||
| `/gsd:list-phase-assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 |
|
||||
| `/gsd:plan-milestone-gaps` | 为审计缺口创建阶段 | 审计发现缺失项后 |
|
||||
| `/gsd:research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 |
|
||||
|
||||
### 现有代码库和工具
|
||||
|
||||
| 命令 | 用途 | 何时使用 |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd:map-codebase` | 分析现有代码库 | 在现有代码上运行 `/gsd:new-project` 之前 |
|
||||
| `/gsd:quick` | 带 GSD 保证的临时任务 | Bug 修复、小功能、配置更改 |
|
||||
| `/gsd:debug [desc]` | 带持久状态的系统化调试 | 出问题时 |
|
||||
| `/gsd:add-todo [desc]` | 捕获想法留待后用 | 会话期间想到什么 |
|
||||
| `/gsd:check-todos` | 列出待处理事项 | 查看捕获的想法 |
|
||||
| `/gsd:settings` | 配置工作流开关和模型配置 | 更改模型、切换代理 |
|
||||
| `/gsd:set-profile <profile>` | 快速切换配置 | 更改成本/质量权衡 |
|
||||
| `/gsd:reapply-patches` | 更新后恢复本地修改 | 如果你有本地编辑,在 `/gsd:update` 后 |
|
||||
|
||||
---
|
||||
|
||||
## 配置参考
|
||||
|
||||
GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。
|
||||
|
||||
### 完整 config.json 模式
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "interactive",
|
||||
"granularity": "standard",
|
||||
"model_profile": "balanced",
|
||||
"planning": {
|
||||
"commit_docs": true,
|
||||
"search_gitignored": false
|
||||
},
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 核心设置
|
||||
|
||||
| 设置 | 选项 | 默认值 | 控制内容 |
|
||||
|---------|---------|---------|------------------|
|
||||
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` 自动批准决策;`interactive` 每步确认 |
|
||||
| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度:范围切分多细(3-5、5-8 或 8-12 个阶段) |
|
||||
| `model_profile` | `quality`, `balanced`, `budget` | `balanced` | 每个代理的模型层级(见下表) |
|
||||
|
||||
### 规划设置
|
||||
|
||||
| 设置 | 选项 | 默认值 | 控制内容 |
|
||||
|---------|---------|---------|------------------|
|
||||
| `planning.commit_docs` | `true`, `false` | `true` | `.planning/` 文件是否提交到 git |
|
||||
| `planning.search_gitignored` | `true`, `false` | `false` | 在广泛搜索中添加 `--no-ignore` 以包含 `.planning/` |
|
||||
|
||||
> **注意:** 如果 `.planning/` 在 `.gitignore` 中,无论配置值如何,`commit_docs` 自动为 `false`。
|
||||
|
||||
### 工作流开关
|
||||
|
||||
| 设置 | 选项 | 默认值 | 控制内容 |
|
||||
|---------|---------|---------|------------------|
|
||||
| `workflow.research` | `true`, `false` | `true` | 规划前的领域调查 |
|
||||
| `workflow.plan_check` | `true`, `false` | `true` | 计划验证循环(最多 3 次迭代) |
|
||||
| `workflow.verifier` | `true`, `false` | `true` | 根据阶段目标的执行后验证 |
|
||||
| `workflow.nyquist_validation` | `true`, `false` | `true` | plan-phase 期间的验证架构研究;第 8 个计划检查维度 |
|
||||
|
||||
在熟悉的领域或需要节省 token 时禁用这些以加速阶段。
|
||||
|
||||
### Git 分支
|
||||
|
||||
| 设置 | 选项 | 默认值 | 控制内容 |
|
||||
|---------|---------|---------|------------------|
|
||||
| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 何时以及如何创建分支 |
|
||||
| `git.phase_branch_template` | 模板字符串 | `gsd/phase-{phase}-{slug}` | 阶段策略的分支名 |
|
||||
| `git.milestone_branch_template` | 模板字符串 | `gsd/{milestone}-{slug}` | 里程碑策略的分支名 |
|
||||
|
||||
**分支策略说明:**
|
||||
|
||||
| 策略 | 创建分支 | 范围 | 适用于 |
|
||||
|----------|---------------|-------|----------|
|
||||
| `none` | 从不 | N/A | 独立开发、简单项目 |
|
||||
| `phase` | 每次 `execute-phase` | 每个阶段一个分支 | 每阶段代码审查、细粒度回滚 |
|
||||
| `milestone` | 第一次 `execute-phase` | 所有阶段共享一个分支 | 发布分支、每个版本一个 PR |
|
||||
|
||||
**模板变量:** `{phase}` = 零填充数字(如 "03"),`{slug}` = 小写连字符名称,`{milestone}` = 版本(如 "v1.0")。
|
||||
|
||||
### 模型配置(每个代理分解)
|
||||
|
||||
| 代理 | `quality` | `balanced` | `budget` |
|
||||
|-------|-----------|------------|----------|
|
||||
| gsd-planner | Opus | Opus | Sonnet |
|
||||
| gsd-roadmapper | Opus | Sonnet | Sonnet |
|
||||
| gsd-executor | Opus | Sonnet | Sonnet |
|
||||
| gsd-phase-researcher | Opus | Sonnet | Haiku |
|
||||
| gsd-project-researcher | Opus | Sonnet | Haiku |
|
||||
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku |
|
||||
| gsd-debugger | Opus | Sonnet | Sonnet |
|
||||
| gsd-codebase-mapper | Sonnet | Haiku | Haiku |
|
||||
| gsd-verifier | Sonnet | Sonnet | Haiku |
|
||||
| gsd-plan-checker | Sonnet | Sonnet | Haiku |
|
||||
| gsd-integration-checker | Sonnet | Sonnet | Haiku |
|
||||
|
||||
**配置理念:**
|
||||
- **quality** —— 所有决策代理使用 Opus,只读验证使用 Sonnet。有配额可用且工作关键时使用。
|
||||
- **balanced** —— 仅规划(架构决策发生的地方)使用 Opus,其他全部使用 Sonnet。这是默认,有充分理由。
|
||||
- **budget** —— 编写代码的使用 Sonnet,研究和验证使用 Haiku。大量工作或不太关键的阶段使用。
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 新项目(完整周期)
|
||||
|
||||
```bash
|
||||
claude --dangerously-skip-permissions
|
||||
/gsd:new-project # 回答问题,配置,批准路线图
|
||||
/clear
|
||||
/gsd:discuss-phase 1 # 锁定你的偏好
|
||||
/gsd:plan-phase 1 # 研究 + 规划 + 验证
|
||||
/gsd:execute-phase 1 # 并行执行
|
||||
/gsd:verify-work 1 # 手动 UAT
|
||||
/clear
|
||||
/gsd:discuss-phase 2 # 对每个阶段重复
|
||||
...
|
||||
/gsd:audit-milestone # 检查所有内容已发布
|
||||
/gsd:complete-milestone # 归档,标记,完成
|
||||
```
|
||||
|
||||
### 从现有文档创建新项目
|
||||
|
||||
```bash
|
||||
/gsd:new-project --auto @prd.md # 从你的文档自动运行研究/需求/路线图
|
||||
/clear
|
||||
/gsd:discuss-phase 1 # 从这里开始正常流程
|
||||
```
|
||||
|
||||
### 现有代码库
|
||||
|
||||
```bash
|
||||
/gsd:map-codebase # 分析现有内容(并行代理)
|
||||
/gsd:new-project # 问题聚焦于你正在添加的内容
|
||||
# (从这里开始正常阶段工作流)
|
||||
```
|
||||
|
||||
### 快速 Bug 修复
|
||||
|
||||
```bash
|
||||
/gsd:quick
|
||||
> "修复移动端 Safari 上登录按钮无响应的问题"
|
||||
```
|
||||
|
||||
### 中断后恢复
|
||||
|
||||
```bash
|
||||
/gsd:progress # 查看你停在哪和接下来做什么
|
||||
# 或
|
||||
/gsd:resume-work # 从上次会话完整恢复上下文
|
||||
```
|
||||
|
||||
### 准备发布
|
||||
|
||||
```bash
|
||||
/gsd:audit-milestone # 检查需求覆盖率,检测存根
|
||||
/gsd:plan-milestone-gaps # 如果审计发现缺口,创建阶段来填补
|
||||
/gsd:complete-milestone # 归档,标记,完成
|
||||
```
|
||||
|
||||
### 速度与质量预设
|
||||
|
||||
| 场景 | 模式 | 粒度 | 配置 | 研究 | 计划检查 | 验证器 |
|
||||
|----------|------|-------|---------|----------|------------|----------|
|
||||
| 原型开发 | `yolo` | `coarse` | `budget` | 关 | 关 | 关 |
|
||||
| 正常开发 | `interactive` | `standard` | `balanced` | 开 | 开 | 开 |
|
||||
| 生产环境 | `interactive` | `fine` | `quality` | 开 | 开 | 开 |
|
||||
|
||||
### 里程碑中途范围变更
|
||||
|
||||
```bash
|
||||
/gsd:add-phase # 向路线图追加新阶段
|
||||
# 或
|
||||
/gsd:insert-phase 3 # 在阶段 3 和 4 之间插入紧急工作
|
||||
# 或
|
||||
/gsd:remove-phase 7 # 移除阶段 7 并重新编号
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### "项目已初始化"
|
||||
|
||||
你运行了 `/gsd:new-project` 但 `.planning/PROJECT.md` 已存在。这是安全检查。如果你想重新开始,先删除 `.planning/` 目录。
|
||||
|
||||
### 长会话期间上下文退化
|
||||
|
||||
在主要命令之间清除上下文窗口:Claude Code 中的 `/clear`。GSD 设计围绕全新上下文 —— 每个子代理获得干净的 200K 窗口。如果主会话质量下降,清除并使用 `/gsd:resume-work` 或 `/gsd:progress` 恢复状态。
|
||||
|
||||
### 计划看起来错误或不一致
|
||||
|
||||
在规划前运行 `/gsd:discuss-phase [N]`。大多数计划质量问题来自 Claude 做出了 `CONTEXT.md` 本可以防止的假设。你也可以运行 `/gsd:list-phase-assumptions [N]` 在提交计划前查看 Claude 打算做什么。
|
||||
|
||||
### 执行失败或产生存根
|
||||
|
||||
检查计划是否太雄心勃勃。计划最多应有 2-3 个任务。如果任务太大,它们超出了单个上下文窗口可以可靠产生的内容。用更小的范围重新规划。
|
||||
|
||||
### 忘记你在哪里
|
||||
|
||||
运行 `/gsd:progress`。它读取所有状态文件,准确告诉你位置和下一步。
|
||||
|
||||
### 执行后需要更改某些内容
|
||||
|
||||
不要重新运行 `/gsd:execute-phase`。使用 `/gsd:quick` 进行针对性修复,或用 `/gsd:verify-work` 通过 UAT 系统识别和修复问题。
|
||||
|
||||
### 模型成本太高
|
||||
|
||||
切换到 budget 配置:`/gsd:set-profile budget`。如果领域对你(或 Claude)熟悉,通过 `/gsd:settings` 禁用研究和计划检查代理。
|
||||
|
||||
### 处理敏感/私有项目
|
||||
|
||||
在 `/gsd:new-project` 期间或通过 `/gsd:settings` 设置 `commit_docs: false`。将 `.planning/` 添加到 `.gitignore`。规划工件保留在本地,从不接触 git。
|
||||
|
||||
### GSD 更新覆盖了我的本地更改
|
||||
|
||||
从 v1.17 开始,安装程序将本地修改的文件备份到 `gsd-local-patches/`。运行 `/gsd:reapply-patches` 将你的更改合并回来。
|
||||
|
||||
### 子代理似乎失败但工作已完成
|
||||
|
||||
存在 Claude Code 分类 bug 的已知解决方法。GSD 的编排器(execute-phase、quick)在报告失败前抽查实际输出。如果你看到失败消息但提交已创建,检查 `git log` —— 工作可能已成功。
|
||||
|
||||
---
|
||||
|
||||
## 恢复快速参考
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
|---------|----------|
|
||||
| 丢失上下文 / 新会话 | `/gsd:resume-work` 或 `/gsd:progress` |
|
||||
| 阶段出错 | `git revert` 阶段提交,然后重新规划 |
|
||||
| 需要更改范围 | `/gsd:add-phase`、`/gsd:insert-phase` 或 `/gsd:remove-phase` |
|
||||
| 里程碑审计发现缺口 | `/gsd:plan-milestone-gaps` |
|
||||
| 出问题了 | `/gsd:debug "描述"` |
|
||||
| 快速针对性修复 | `/gsd:quick` |
|
||||
| 计划与你的愿景不符 | `/gsd:discuss-phase [N]` 然后重新规划 |
|
||||
| 成本过高 | `/gsd:set-profile budget` 和 `/gsd:settings` 关闭代理 |
|
||||
| 更新破坏了本地更改 | `/gsd:reapply-patches` |
|
||||
|
||||
---
|
||||
|
||||
## 项目文件结构
|
||||
|
||||
供参考,这是 GSD 在你的项目中创建的内容:
|
||||
|
||||
```
|
||||
.planning/
|
||||
PROJECT.md # 项目愿景和上下文(始终加载)
|
||||
REQUIREMENTS.md # 界定 v1/v2 需求及 ID
|
||||
ROADMAP.md # 带状态跟踪的阶段分解
|
||||
STATE.md # 决策、阻塞项、会话记忆
|
||||
config.json # 工作流配置
|
||||
MILESTONES.md # 已完成里程碑归档
|
||||
research/ # 来自 /gsd:new-project 的领域研究
|
||||
todos/
|
||||
pending/ # 等待处理的捕获想法
|
||||
done/ # 已完成的待办事项
|
||||
debug/ # 活跃调试会话
|
||||
resolved/ # 已归档的调试会话
|
||||
codebase/ # 现有代码库映射(来自 /gsd:map-codebase)
|
||||
phases/
|
||||
XX-phase-name/
|
||||
XX-YY-PLAN.md # 原子执行计划
|
||||
XX-YY-SUMMARY.md # 执行结果和决策
|
||||
CONTEXT.md # 你的实现偏好
|
||||
RESEARCH.md # 生态研究发现
|
||||
VERIFICATION.md # 执行后验证结果
|
||||
```
|
||||
450
docs/zh-CN/references/checkpoints.md
Normal file
450
docs/zh-CN/references/checkpoints.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# 检查点
|
||||
|
||||
计划自主执行。检查点用于规范化需要人工验证或决策的交互点。
|
||||
|
||||
**核心原则:** Claude 用 CLI/API 自动化一切。检查点用于验证和决策,而非手动工作。
|
||||
|
||||
**黄金法则:**
|
||||
1. **如果 Claude 能运行,Claude 就运行** - 绝不让用户执行 CLI 命令、启动服务器或运行构建
|
||||
2. **Claude 设置验证环境** - 启动开发服务器、填充数据库、配置环境变量
|
||||
3. **用户只做需要人工判断的事** - 视觉检查、UX 评估、"这个感觉对吗?"
|
||||
4. **密钥来自用户,自动化来自 Claude** - 询问 API 密钥,然后 Claude 通过 CLI 使用它们
|
||||
5. **自动模式绕过验证/决策检查点** — 当 config 中 `workflow._auto_chain_active` 或 `workflow.auto_advance` 为 true 时:human-verify 自动批准,decision 自动选择第一个选项,human-action 仍会停止(认证门控无法自动化)
|
||||
|
||||
## 检查点类型
|
||||
|
||||
### checkpoint:human-verify(最常见 - 90%)
|
||||
|
||||
**何时使用:** Claude 完成自动化工作,人工确认其正常工作。
|
||||
|
||||
**用于:**
|
||||
- 视觉 UI 检查(布局、样式、响应式)
|
||||
- 交互流程(点击向导、测试用户流程)
|
||||
- 功能验证(功能按预期工作)
|
||||
- 音频/视频播放质量
|
||||
- 动画流畅度
|
||||
- 无障碍测试
|
||||
|
||||
**结构:**
|
||||
```xml
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>[Claude 自动化并部署/构建的内容]</what-built>
|
||||
<how-to-verify>
|
||||
[测试的确切步骤 - URL、命令、预期行为]
|
||||
</how-to-verify>
|
||||
<resume-signal>[如何继续 - "approved"、"yes" 或描述问题]</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
**示例:UI 组件(展示关键模式:Claude 在检查点之前启动服务器)**
|
||||
```xml
|
||||
<task type="auto">
|
||||
<name>构建响应式仪表板布局</name>
|
||||
<files>src/components/Dashboard.tsx, src/app/dashboard/page.tsx</files>
|
||||
<action>创建带侧边栏、标题和内容区域的仪表板。使用 Tailwind 响应式类处理移动端。</action>
|
||||
<verify>npm run build 成功,无 TypeScript 错误</verify>
|
||||
<done>仪表板组件构建无错误</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>启动开发服务器用于验证</name>
|
||||
<action>在后台运行 `npm run dev`,等待 "ready" 消息,捕获端口</action>
|
||||
<verify>curl http://localhost:3000 返回 200</verify>
|
||||
<done>开发服务器运行于 http://localhost:3000</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>响应式仪表板布局 - 开发服务器运行于 http://localhost:3000</what-built>
|
||||
<how-to-verify>
|
||||
访问 http://localhost:3000/dashboard 并验证:
|
||||
1. 桌面端 (>1024px): 左侧边栏,右侧内容,顶部标题
|
||||
2. 平板端 (768px): 侧边栏折叠为汉堡菜单
|
||||
3. 移动端 (375px): 单列布局,出现底部导航
|
||||
4. 任何尺寸无布局偏移或水平滚动
|
||||
</how-to-verify>
|
||||
<resume-signal>输入 "approved" 或描述布局问题</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
### checkpoint:decision(9%)
|
||||
|
||||
**何时使用:** 人工必须做出影响实现方向的选择。
|
||||
|
||||
**用于:**
|
||||
- 技术选型(哪个认证提供商、哪个数据库)
|
||||
- 架构决策(monorepo 还是独立仓库)
|
||||
- 设计选择(配色方案、布局方式)
|
||||
- 功能优先级(构建哪个变体)
|
||||
- 数据模型决策(模式结构)
|
||||
|
||||
**结构:**
|
||||
```xml
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<decision>[正在决策的内容]</decision>
|
||||
<context>[为什么这个决策重要]</context>
|
||||
<options>
|
||||
<option id="option-a">
|
||||
<name>[选项名称]</name>
|
||||
<pros>[好处]</pros>
|
||||
<cons>[权衡]</cons>
|
||||
</option>
|
||||
<option id="option-b">
|
||||
<name>[选项名称]</name>
|
||||
<pros>[好处]</pros>
|
||||
<cons>[权衡]</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>[如何表明选择]</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
**示例:认证提供商选择**
|
||||
```xml
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<decision>选择认证提供商</decision>
|
||||
<context>
|
||||
应用需要用户认证。三个可靠选项各有权衡。
|
||||
</context>
|
||||
<options>
|
||||
<option id="supabase">
|
||||
<name>Supabase Auth</name>
|
||||
<pros>与我们使用的 Supabase DB 内置集成,慷慨的免费额度,行级安全集成</pros>
|
||||
<cons>UI 定制性较差,绑定 Supabase 生态</cons>
|
||||
</option>
|
||||
<option id="clerk">
|
||||
<name>Clerk</name>
|
||||
<pros>精美的预构建 UI,最佳开发体验,优秀文档</pros>
|
||||
<cons>10k MAU 后付费,供应商锁定</cons>
|
||||
</option>
|
||||
<option id="nextauth">
|
||||
<name>NextAuth.js</name>
|
||||
<pros>免费,自托管,最大控制权,广泛采用</pros>
|
||||
<cons>更多设置工作,需自行管理安全更新,UI 需自己构建</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>选择:supabase、clerk 或 nextauth</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
### checkpoint:human-action(1% - 罕见)
|
||||
|
||||
**何时使用:** 操作没有 CLI/API 且需要仅人工交互,或者 Claude 在自动化过程中遇到认证门控。
|
||||
|
||||
**仅用于:**
|
||||
- **认证门控** - Claude 尝试了 CLI/API 但需要凭证(这不是失败)
|
||||
- 邮箱验证链接(点击邮件)
|
||||
- 短信两步验证码(手机验证)
|
||||
- 人工账户审批(平台需要人工审核)
|
||||
- 信用卡 3D Secure 流程(基于 Web 的支付授权)
|
||||
- OAuth 应用审批(基于 Web 的审批)
|
||||
|
||||
**不要用于预定的手动工作:**
|
||||
- 部署(使用 CLI - 如需要则认证门控)
|
||||
- 创建 webhooks/数据库(使用 API/CLI - 如需要则认证门控)
|
||||
- 运行构建/测试(使用 Bash 工具)
|
||||
- 创建文件(使用 Write 工具)
|
||||
|
||||
**结构:**
|
||||
```xml
|
||||
<task type="checkpoint:human-action" gate="blocking">
|
||||
<action>[人工必须做什么 - Claude 已完成所有可自动化的]</action>
|
||||
<instructions>
|
||||
[Claude 已自动化的内容]
|
||||
[需要人工操作的一件事]
|
||||
</instructions>
|
||||
<verification>[Claude 之后可以检查的内容]</verification>
|
||||
<resume-signal>[如何继续]</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
**示例:认证门控(动态检查点)**
|
||||
```xml
|
||||
<task type="auto">
|
||||
<name>部署到 Vercel</name>
|
||||
<files>.vercel/, vercel.json</files>
|
||||
<action>运行 `vercel --yes` 进行部署</action>
|
||||
<verify>vercel ls 显示部署,curl 返回 200</verify>
|
||||
</task>
|
||||
|
||||
<!-- 如果 vercel 返回 "Error: Not authenticated",Claude 即时创建检查点 -->
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking">
|
||||
<action>认证 Vercel CLI 以便我继续部署</action>
|
||||
<instructions>
|
||||
我尝试部署但收到认证错误。
|
||||
运行:vercel login
|
||||
这将打开你的浏览器 - 完成认证流程。
|
||||
</instructions>
|
||||
<verification>vercel whoami 返回你的账户邮箱</verification>
|
||||
<resume-signal>认证完成后输入 "done"</resume-signal>
|
||||
</task>
|
||||
|
||||
<!-- 认证后,Claude 重试部署 -->
|
||||
|
||||
<task type="auto">
|
||||
<name>重试 Vercel 部署</name>
|
||||
<action>运行 `vercel --yes`(已认证)</action>
|
||||
<verify>vercel ls 显示部署,curl 返回 200</verify>
|
||||
</task>
|
||||
```
|
||||
|
||||
**关键区别:** 认证门控是 Claude 遇到认证错误时动态创建的。不是预定的 — Claude 先自动化,只有在被阻止时才请求凭证。
|
||||
|
||||
## 执行协议
|
||||
|
||||
当 Claude 遇到 `type="checkpoint:*"` 时:
|
||||
|
||||
1. **立即停止** - 不继续下一个任务
|
||||
2. **清晰显示检查点** 使用下面的格式
|
||||
3. **等待用户响应** - 不幻想完成
|
||||
4. **如可能则验证** - 检查文件、运行测试、任何指定的内容
|
||||
5. **恢复执行** - 仅在确认后继续下一个任务
|
||||
|
||||
**对于 checkpoint:human-verify:**
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: 需要验证 ║
|
||||
╚═══════════════════════════════════════════════════════╝
|
||||
|
||||
进度: 5/8 任务完成
|
||||
任务: 响应式仪表板布局
|
||||
|
||||
已构建: /dashboard 的响应式仪表板
|
||||
|
||||
如何验证:
|
||||
1. 访问: http://localhost:3000/dashboard
|
||||
2. 桌面端 (>1024px): 侧边栏可见,内容填充剩余空间
|
||||
3. 平板端 (768px): 侧边栏折叠为图标
|
||||
4. 移动端 (375px): 侧边栏隐藏,出现汉堡菜单
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
→ 你的操作: 输入 "approved" 或描述问题
|
||||
────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
**对于 checkpoint:decision:**
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: 需要决策 ║
|
||||
╚═══════════════════════════════════════════════════════╝
|
||||
|
||||
进度: 2/6 任务完成
|
||||
任务: 选择认证提供商
|
||||
|
||||
决策: 我们应该使用哪个认证提供商?
|
||||
|
||||
上下文: 需要用户认证。三个选项各有权衡。
|
||||
|
||||
选项:
|
||||
1. supabase - 与我们的数据库内置集成,免费额度
|
||||
优点: 行级安全集成,慷慨的免费额度
|
||||
缺点: UI 定制性较差,生态锁定
|
||||
|
||||
2. clerk - 最佳 DX,10k 用户后付费
|
||||
优点: 精美的预构建 UI,优秀文档
|
||||
缺点: 供应商锁定,规模化时价格问题
|
||||
|
||||
3. nextauth - 自托管,最大控制权
|
||||
优点: 免费,无供应商锁定,广泛采用
|
||||
缺点: 更多设置工作,自行 DIY 安全更新
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
→ 你的操作: 选择 supabase、clerk 或 nextauth
|
||||
────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
## 认证门控
|
||||
|
||||
**认证门控 = Claude 尝试了 CLI/API,收到认证错误。** 不是失败 — 是需要人工输入来解除阻止的门控。
|
||||
|
||||
**模式:** Claude 尝试自动化 → 认证错误 → 创建 checkpoint:human-action → 用户认证 → Claude 重试 → 继续
|
||||
|
||||
**门控协议:**
|
||||
1. 认识到这不是失败 - 缺少认证是正常的
|
||||
2. 停止当前任务 - 不要反复重试
|
||||
3. 动态创建 checkpoint:human-action
|
||||
4. 提供确切的认证步骤
|
||||
5. 验证认证有效
|
||||
6. 重试原始任务
|
||||
7. 正常继续
|
||||
|
||||
**关键区别:**
|
||||
- 预定的检查点:"我需要你做 X"(错误 - Claude 应该自动化)
|
||||
- 认证门控:"我尝试自动化 X 但需要凭证"(正确 - 解除自动化阻止)
|
||||
|
||||
## 自动化参考
|
||||
|
||||
**规则:** 如果有 CLI/API,Claude 就做。绝不让人工执行可自动化的工作。
|
||||
|
||||
### 服务 CLI 参考
|
||||
|
||||
| 服务 | CLI/API | 关键命令 | 认证门控 |
|
||||
|------|---------|----------|----------|
|
||||
| Vercel | `vercel` | `--yes`, `env add`, `--prod`, `ls` | `vercel login` |
|
||||
| Railway | `railway` | `init`, `up`, `variables set` | `railway login` |
|
||||
| Fly | `fly` | `launch`, `deploy`, `secrets set` | `fly auth login` |
|
||||
| Stripe | `stripe` + API | `listen`, `trigger`, API 调用 | .env 中的 API key |
|
||||
| Supabase | `supabase` | `init`, `link`, `db push`, `gen types` | `supabase login` |
|
||||
| Upstash | `upstash` | `redis create`, `redis get` | `upstash auth login` |
|
||||
| PlanetScale | `pscale` | `database create`, `branch create` | `pscale auth login` |
|
||||
| GitHub | `gh` | `repo create`, `pr create`, `secret set` | `gh auth login` |
|
||||
| Node | `npm`/`pnpm` | `install`, `run build`, `test`, `run dev` | N/A |
|
||||
| Xcode | `xcodebuild` | `-project`, `-scheme`, `build`, `test` | N/A |
|
||||
| Convex | `npx convex` | `dev`, `deploy`, `env set`, `env get` | `npx convex login` |
|
||||
|
||||
### 环境变量自动化
|
||||
|
||||
**Env 文件:** 使用 Write/Edit 工具。绝不让用户手动创建 .env。
|
||||
|
||||
**通过 CLI 的仪表板环境变量:**
|
||||
|
||||
| 平台 | CLI 命令 | 示例 |
|
||||
|------|----------|------|
|
||||
| Convex | `npx convex env set` | `npx convex env set OPENAI_API_KEY sk-...` |
|
||||
| Vercel | `vercel env add` | `vercel env add STRIPE_KEY production` |
|
||||
| Railway | `railway variables set` | `railway variables set API_KEY=value` |
|
||||
| Fly | `fly secrets set` | `fly secrets set DATABASE_URL=...` |
|
||||
| Supabase | `supabase secrets set` | `supabase secrets set MY_SECRET=value` |
|
||||
|
||||
### 开发服务器自动化
|
||||
|
||||
| 框架 | 启动命令 | 就绪信号 | 默认 URL |
|
||||
|------|----------|----------|----------|
|
||||
| Next.js | `npm run dev` | "Ready in" 或 "started server" | http://localhost:3000 |
|
||||
| Vite | `npm run dev` | "ready in" | http://localhost:5173 |
|
||||
| Convex | `npx convex dev` | "Convex functions ready" | N/A(仅后端)|
|
||||
| Express | `npm start` | "listening on port" | http://localhost:3000 |
|
||||
| Django | `python manage.py runserver` | "Starting development server" | http://localhost:8000 |
|
||||
|
||||
**服务器生命周期:**
|
||||
```bash
|
||||
# 后台运行,捕获 PID
|
||||
npm run dev &
|
||||
DEV_SERVER_PID=$!
|
||||
|
||||
# 等待就绪(最多 30s)
|
||||
timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done'
|
||||
```
|
||||
|
||||
**端口冲突:** 终止陈旧进程(`lsof -ti:3000 | xargs kill`)或使用备用端口(`--port 3001`)。
|
||||
|
||||
**服务器保持运行** 直到检查点结束。仅在计划完成、切换到生产环境或端口需要用于不同服务时终止。
|
||||
|
||||
### CLI 安装处理
|
||||
|
||||
| CLI | 自动安装? | 命令 |
|
||||
|-----|------------|------|
|
||||
| npm/pnpm/yarn | 否 - 询问用户 | 用户选择包管理器 |
|
||||
| vercel | 是 | `npm i -g vercel` |
|
||||
| gh (GitHub) | 是 | `brew install gh` (macOS) 或 `apt install gh` (Linux) |
|
||||
| stripe | 是 | `npm i -g stripe` |
|
||||
| supabase | 是 | `npm i -g supabase` |
|
||||
| convex | 否 - 使用 npx | `npx convex`(无需安装)|
|
||||
| fly | 是 | `brew install flyctl` 或 curl 安装器 |
|
||||
| railway | 是 | `npm i -g @railway/cli` |
|
||||
|
||||
**协议:** 尝试命令 → "command not found" → 可自动安装?→ 是:静默安装,重试 → 否:检查点请求用户安装。
|
||||
|
||||
## 检查点前自动化失败处理
|
||||
|
||||
| 失败 | 响应 |
|
||||
|------|------|
|
||||
| 服务器无法启动 | 检查错误,修复问题,重试(不进入检查点)|
|
||||
| 端口被占用 | 终止陈旧进程或使用备用端口 |
|
||||
| 缺少依赖 | 运行 `npm install`,重试 |
|
||||
| 构建错误 | 先修复错误(是 bug,不是检查点问题)|
|
||||
| 认证错误 | 创建认证门控检查点 |
|
||||
| 网络超时 | 带退避重试,如果持续则检查点 |
|
||||
|
||||
**绝不呈现验证环境损坏的检查点。** 如果 `curl localhost:3000` 失败,不要让用户"访问 localhost:3000"。
|
||||
|
||||
## 可自动化快速参考
|
||||
|
||||
| 操作 | 可自动化?| Claude 做?|
|
||||
|------|------------|------------|
|
||||
| 部署到 Vercel | 是 (`vercel`) | 是 |
|
||||
| 创建 Stripe webhook | 是 (API) | 是 |
|
||||
| 写入 .env 文件 | 是 (Write 工具) | 是 |
|
||||
| 创建 Upstash DB | 是 (`upstash`) | 是 |
|
||||
| 运行测试 | 是 (`npm test`) | 是 |
|
||||
| 启动开发服务器 | 是 (`npm run dev`) | 是 |
|
||||
| 添加环境变量到 Convex | 是 (`npx convex env set`) | 是 |
|
||||
| 添加环境变量到 Vercel | 是 (`vercel env add`) | 是 |
|
||||
| 填充数据库 | 是 (CLI/API) | 是 |
|
||||
| 点击邮件验证链接 | 否 | 否 |
|
||||
| 输入带 3DS 的信用卡 | 否 | 否 |
|
||||
| 在浏览器中完成 OAuth | 否 | 否 |
|
||||
| 视觉验证 UI 是否正确 | 否 | 否 |
|
||||
| 测试交互式用户流程 | 否 | 否 |
|
||||
|
||||
## 反模式
|
||||
|
||||
### ❌ 错误:让用户启动开发服务器
|
||||
```xml
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>仪表板组件</what-built>
|
||||
<how-to-verify>
|
||||
1. 运行: npm run dev
|
||||
2. 访问: http://localhost:3000/dashboard
|
||||
3. 检查布局是否正确
|
||||
</how-to-verify>
|
||||
</task>
|
||||
```
|
||||
**为什么错误:** Claude 可以运行 `npm run dev`。用户应该只访问 URL,不执行命令。
|
||||
|
||||
### ✅ 正确:Claude 启动服务器,用户访问
|
||||
```xml
|
||||
<task type="auto">
|
||||
<name>启动开发服务器</name>
|
||||
<action>在后台运行 `npm run dev`</action>
|
||||
<verify>curl localhost:3000 返回 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>http://localhost:3000/dashboard 的仪表板(服务器运行中)</what-built>
|
||||
<how-to-verify>
|
||||
访问 http://localhost:3000/dashboard 并验证:
|
||||
1. 布局匹配设计
|
||||
2. 无控制台错误
|
||||
</how-to-verify>
|
||||
</task>
|
||||
```
|
||||
|
||||
### ❌ 错误:让用户部署 / ✅ 正确:Claude 自动化
|
||||
```xml
|
||||
<!-- 错误:让用户通过仪表板部署 -->
|
||||
<task type="checkpoint:human-action" gate="blocking">
|
||||
<action>部署到 Vercel</action>
|
||||
<instructions>访问 vercel.com/new → 导入仓库 → 点击部署 → 复制 URL</instructions>
|
||||
</task>
|
||||
|
||||
<!-- 正确:Claude 部署,用户验证 -->
|
||||
<task type="auto">
|
||||
<name>部署到 Vercel</name>
|
||||
<action>运行 `vercel --yes`。捕获 URL。</action>
|
||||
<verify>vercel ls 显示部署,curl 返回 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify">
|
||||
<what-built>已部署到 {url}</what-built>
|
||||
<how-to-verify>访问 {url},检查首页加载</how-to-verify>
|
||||
<resume-signal>输入 "approved"</resume-signal>
|
||||
</task>
|
||||
```
|
||||
|
||||
## 摘要
|
||||
|
||||
检查点规范化人工介入点用于验证和决策,而非手动工作。
|
||||
|
||||
**黄金法则:** 如果 Claude 能自动化它,Claude 就必须自动化它。
|
||||
|
||||
**检查点优先级:**
|
||||
1. **checkpoint:human-verify**(90%)- Claude 自动化一切,人工确认视觉/功能正确性
|
||||
2. **checkpoint:decision**(9%)- 人工做出架构/技术选择
|
||||
3. **checkpoint:human-action**(1%)- 真正无法避免的、没有 API/CLI 的手动步骤
|
||||
|
||||
**何时不用检查点:**
|
||||
- Claude 可以编程验证的事情(测试、构建)
|
||||
- 文件操作(Claude 可以读取文件)
|
||||
- 代码正确性(测试和静态分析)
|
||||
- 任何可通过 CLI/API 自动化的内容
|
||||
249
docs/zh-CN/references/continuation-format.md
Normal file
249
docs/zh-CN/references/continuation-format.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 续接格式
|
||||
|
||||
完成命令或工作流后展示下一步的标准格式。
|
||||
|
||||
## 核心结构
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**{标识符}: {名称}** — {单行描述}
|
||||
|
||||
`{可复制粘贴的命令}`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
|
||||
**也可选:**
|
||||
- `{备选项 1}` — 描述
|
||||
- `{备选项 2}` — 描述
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## 格式规则
|
||||
|
||||
1. **始终展示它是什么** — 名称 + 描述,绝不仅仅是一个命令路径
|
||||
2. **从源文件拉取上下文** — ROADMAP.md 用于阶段,PLAN.md `<objective>` 用于计划
|
||||
3. **命令用内联代码** — 反引号,易于复制粘贴,渲染为可点击链接
|
||||
4. **`/clear` 说明** — 始终包含,保持简洁但解释原因
|
||||
5. **用"也可选"而非"其他选项"** — 听起来更像应用
|
||||
6. **视觉分隔符** — 上下用 `---` 使其突出
|
||||
|
||||
## 变体
|
||||
|
||||
### 执行下一个计划
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh
|
||||
|
||||
`/gsd:execute-phase 2`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
|
||||
**也可选:**
|
||||
- 执行前审查计划
|
||||
- `/gsd:list-phase-assumptions 2` — 检查假设
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 执行阶段中最后一个计划
|
||||
|
||||
添加注释说明这是最后一个计划以及接下来是什么:
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh
|
||||
<sub>阶段 2 的最后一个计划</sub>
|
||||
|
||||
`/gsd:execute-phase 2`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
|
||||
**完成后:**
|
||||
- 阶段 2 → 阶段 3 过渡
|
||||
- 下一步:**阶段 3: 核心功能** — 用户仪表板和设置
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 规划阶段
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程
|
||||
|
||||
`/gsd:plan-phase 2`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
|
||||
**也可选:**
|
||||
- `/gsd:discuss-phase 2` — 先收集上下文
|
||||
- `/gsd:research-phase 2` — 调查未知项
|
||||
- 审查路线图
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 阶段完成,准备下一步
|
||||
|
||||
在下一步操作前显示完成状态:
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ✓ 阶段 2 完成
|
||||
|
||||
3/3 计划已执行
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**阶段 3: 核心功能** — 用户仪表板、设置和数据导出
|
||||
|
||||
`/gsd:plan-phase 3`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
|
||||
**也可选:**
|
||||
- `/gsd:discuss-phase 3` — 先收集上下文
|
||||
- `/gsd:research-phase 3` — 调查未知项
|
||||
- 回顾阶段 2 构建的内容
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 多个同等选项
|
||||
|
||||
当没有明确的主要操作时:
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**阶段 3: 核心功能** — 用户仪表板、设置和数据导出
|
||||
|
||||
**直接规划:** `/gsd:plan-phase 3`
|
||||
|
||||
**先讨论上下文:** `/gsd:discuss-phase 3`
|
||||
|
||||
**研究未知项:** `/gsd:research-phase 3`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### 里程碑完成
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## 🎉 里程碑 v1.0 完成
|
||||
|
||||
全部 4 个阶段已发布
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**开始 v1.1** — 提问 → 研究 → 需求 → 路线图
|
||||
|
||||
`/gsd:new-milestone`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## 拉取上下文
|
||||
|
||||
### 用于阶段(从 ROADMAP.md):
|
||||
|
||||
```markdown
|
||||
### 阶段 2: 认证
|
||||
**目标**: 带刷新令牌的 JWT 登录流程
|
||||
```
|
||||
|
||||
提取:`**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程`
|
||||
|
||||
### 用于计划(从 ROADMAP.md):
|
||||
|
||||
```markdown
|
||||
计划:
|
||||
- [ ] 02-03: 添加刷新令牌轮换
|
||||
```
|
||||
|
||||
或从 PLAN.md `<objective>`:
|
||||
|
||||
```xml
|
||||
<objective>
|
||||
添加带滑动过期窗口的刷新令牌轮换。
|
||||
|
||||
目的: 在不影响安全性的前提下延长会话生命周期。
|
||||
</objective>
|
||||
```
|
||||
|
||||
提取:`**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh`
|
||||
|
||||
## 反模式
|
||||
|
||||
### 不要:仅命令(无上下文)
|
||||
|
||||
```
|
||||
## 继续
|
||||
|
||||
运行 `/clear`,然后粘贴:
|
||||
/gsd:execute-phase 2
|
||||
```
|
||||
|
||||
用户不知道 02-03 是关于什么的。
|
||||
|
||||
### 不要:缺少 /clear 说明
|
||||
|
||||
```
|
||||
`/gsd:plan-phase 3`
|
||||
|
||||
先运行 /clear。
|
||||
```
|
||||
|
||||
没有解释原因。用户可能跳过。
|
||||
|
||||
### 不要:"其他选项" 措辞
|
||||
|
||||
```
|
||||
其他选项:
|
||||
- 审查路线图
|
||||
```
|
||||
|
||||
听起来像是事后补充。用"也可选:"替代。
|
||||
|
||||
### 不要:用围栏代码块展示命令
|
||||
|
||||
```
|
||||
```
|
||||
/gsd:plan-phase 3
|
||||
```
|
||||
```
|
||||
|
||||
模板内的围栏代码块会造成嵌套歧义。用内联反引号替代。
|
||||
65
docs/zh-CN/references/decimal-phase-calculation.md
Normal file
65
docs/zh-CN/references/decimal-phase-calculation.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 小数阶段计算
|
||||
|
||||
为紧急插入计算下一个小数阶段编号。
|
||||
|
||||
## 使用 gsd-tools
|
||||
|
||||
```bash
|
||||
# 获取阶段 6 之后的下一个小数阶段
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 6
|
||||
```
|
||||
|
||||
输出:
|
||||
```json
|
||||
{
|
||||
"found": true,
|
||||
"base_phase": "06",
|
||||
"next": "06.1",
|
||||
"existing": []
|
||||
}
|
||||
```
|
||||
|
||||
已有小数时:
|
||||
```json
|
||||
{
|
||||
"found": true,
|
||||
"base_phase": "06",
|
||||
"next": "06.3",
|
||||
"existing": ["06.1", "06.2"]
|
||||
}
|
||||
```
|
||||
|
||||
## 提取值
|
||||
|
||||
```bash
|
||||
DECIMAL_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}")
|
||||
DECIMAL_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.next')
|
||||
BASE_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.base_phase')
|
||||
```
|
||||
|
||||
或使用 --raw 标志:
|
||||
```bash
|
||||
DECIMAL_PHASE=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}" --raw)
|
||||
# 返回: 06.1
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
| 已有阶段 | 下一个阶段 |
|
||||
|----------|------------|
|
||||
| 仅 06 | 06.1 |
|
||||
| 06, 06.1 | 06.2 |
|
||||
| 06, 06.1, 06.2 | 06.3 |
|
||||
| 06, 06.1, 06.3(有空缺)| 06.4 |
|
||||
|
||||
## 目录命名
|
||||
|
||||
小数阶段目录使用完整的小数编号:
|
||||
|
||||
```bash
|
||||
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$DESCRIPTION" --raw)
|
||||
PHASE_DIR=".planning/phases/${DECIMAL_PHASE}-${SLUG}"
|
||||
mkdir -p "$PHASE_DIR"
|
||||
```
|
||||
|
||||
示例:`.planning/phases/06.1-fix-critical-auth-bug/`
|
||||
248
docs/zh-CN/references/git-integration.md
Normal file
248
docs/zh-CN/references/git-integration.md
Normal file
@@ -0,0 +1,248 @@
|
||||
<overview>
|
||||
GSD 框架的 Git 集成。
|
||||
</overview>
|
||||
|
||||
<core_principle>
|
||||
|
||||
**提交结果,而非过程。**
|
||||
|
||||
git 日志应该读起来像是发布内容的变更日志,而不是规划活动的日记。
|
||||
</core_principle>
|
||||
|
||||
<commit_points>
|
||||
|
||||
| 事件 | 提交? | 原因 |
|
||||
| ----------------------- | ------- | ------------------------------------------------ |
|
||||
| BRIEF + ROADMAP 创建 | 是 | 项目初始化 |
|
||||
| PLAN.md 创建 | 否 | 中间产物 - 与计划完成一起提交 |
|
||||
| RESEARCH.md 创建 | 否 | 中间产物 |
|
||||
| DISCOVERY.md 创建 | 否 | 中间产物 |
|
||||
| **任务完成** | 是 | 原子工作单元(每个任务 1 个提交) |
|
||||
| **计划完成** | 是 | 元数据提交(SUMMARY + STATE + ROADMAP) |
|
||||
| 交接创建 | 是 | WIP 状态保留 |
|
||||
|
||||
</commit_points>
|
||||
|
||||
<git_check>
|
||||
|
||||
```bash
|
||||
[ -d .git ] && echo "GIT_EXISTS" || echo "NO_GIT"
|
||||
```
|
||||
|
||||
如果 NO_GIT:静默运行 `git init`。GSD 项目总是有自己的仓库。
|
||||
</git_check>
|
||||
|
||||
<commit_formats>
|
||||
|
||||
<format name="initialization">
|
||||
## 项目初始化(brief + roadmap 一起)
|
||||
|
||||
```
|
||||
docs: initialize [project-name] ([N] phases)
|
||||
|
||||
[PROJECT.md 中的一句话描述]
|
||||
|
||||
Phases:
|
||||
1. [phase-name]: [goal]
|
||||
2. [phase-name]: [goal]
|
||||
3. [phase-name]: [goal]
|
||||
```
|
||||
|
||||
提交内容:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [project-name] ([N] phases)" --files .planning/
|
||||
```
|
||||
|
||||
</format>
|
||||
|
||||
<format name="task-completion">
|
||||
## 任务完成(计划执行期间)
|
||||
|
||||
每个任务在完成后立即获得自己的提交。
|
||||
|
||||
```
|
||||
{type}({phase}-{plan}): {task-name}
|
||||
|
||||
- [关键变更 1]
|
||||
- [关键变更 2]
|
||||
- [关键变更 3]
|
||||
```
|
||||
|
||||
**提交类型:**
|
||||
- `feat` - 新功能/功能
|
||||
- `fix` - Bug 修复
|
||||
- `test` - 仅测试(TDD RED 阶段)
|
||||
- `refactor` - 代码清理(TDD REFACTOR 阶段)
|
||||
- `perf` - 性能改进
|
||||
- `chore` - 依赖、配置、工具
|
||||
|
||||
**示例:**
|
||||
|
||||
```bash
|
||||
# 标准任务
|
||||
git add src/api/auth.ts src/types/user.ts
|
||||
git commit -m "feat(08-02): create user registration endpoint
|
||||
|
||||
- POST /auth/register validates email and password
|
||||
- Checks for duplicate users
|
||||
- Returns JWT token on success
|
||||
"
|
||||
|
||||
# TDD 任务 - RED 阶段
|
||||
git add src/__tests__/jwt.test.ts
|
||||
git commit -m "test(07-02): add failing test for JWT generation
|
||||
|
||||
- Tests token contains user ID claim
|
||||
- Tests token expires in 1 hour
|
||||
- Tests signature verification
|
||||
"
|
||||
|
||||
# TDD 任务 - GREEN 阶段
|
||||
git add src/utils/jwt.ts
|
||||
git commit -m "feat(07-02): implement JWT generation
|
||||
|
||||
- Uses jose library for signing
|
||||
- Includes user ID and expiry claims
|
||||
- Signs with HS256 algorithm
|
||||
"
|
||||
```
|
||||
|
||||
</format>
|
||||
|
||||
<format name="plan-completion">
|
||||
## 计划完成(所有任务完成后)
|
||||
|
||||
所有任务提交后,最后一个元数据提交捕获计划完成。
|
||||
|
||||
```
|
||||
docs({phase}-{plan}): complete [plan-name] plan
|
||||
|
||||
Tasks completed: [N]/[N]
|
||||
- [Task 1 name]
|
||||
- [Task 2 name]
|
||||
- [Task 3 name]
|
||||
|
||||
SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md
|
||||
```
|
||||
|
||||
提交内容:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
**注意:** 代码文件不包含 - 已按任务提交。
|
||||
|
||||
</format>
|
||||
|
||||
<format name="handoff">
|
||||
## 交接(WIP)
|
||||
|
||||
```
|
||||
wip: [phase-name] paused at task [X]/[Y]
|
||||
|
||||
Current: [task name]
|
||||
[如果阻塞:] Blocked: [reason]
|
||||
```
|
||||
|
||||
提交内容:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
|
||||
```
|
||||
|
||||
</format>
|
||||
</commit_formats>
|
||||
|
||||
<example_log>
|
||||
|
||||
**旧方法(每个计划提交):**
|
||||
```
|
||||
a7f2d1 feat(checkout): Stripe payments with webhook verification
|
||||
3e9c4b feat(products): catalog with search, filters, and pagination
|
||||
8a1b2c feat(auth): JWT with refresh rotation using jose
|
||||
5c3d7e feat(foundation): Next.js 15 + Prisma + Tailwind scaffold
|
||||
2f4a8d docs: initialize ecommerce-app (5 phases)
|
||||
```
|
||||
|
||||
**新方法(每个任务提交):**
|
||||
```
|
||||
# Phase 04 - Checkout
|
||||
1a2b3c docs(04-01): complete checkout flow plan
|
||||
4d5e6f feat(04-01): add webhook signature verification
|
||||
7g8h9i feat(04-01): implement payment session creation
|
||||
0j1k2l feat(04-01): create checkout page component
|
||||
|
||||
# Phase 03 - Products
|
||||
3m4n5o docs(03-02): complete product listing plan
|
||||
6p7q8r feat(03-02): add pagination controls
|
||||
9s0t1u feat(03-02): implement search and filters
|
||||
2v3w4x feat(03-01): create product catalog schema
|
||||
|
||||
# Phase 02 - Auth
|
||||
5y6z7a docs(02-02): complete token refresh plan
|
||||
8b9c0d feat(02-02): implement refresh token rotation
|
||||
1e2f3g test(02-02): add failing test for token refresh
|
||||
4h5i6j docs(02-01): complete JWT setup plan
|
||||
7k8l9m feat(02-01): add JWT generation and validation
|
||||
0n1o2p chore(02-01): install jose library
|
||||
|
||||
# Phase 01 - Foundation
|
||||
3q4r5s docs(01-01): complete scaffold plan
|
||||
6t7u8v feat(01-01): configure Tailwind and globals
|
||||
9w0x1y feat(01-01): set up Prisma with database
|
||||
2z3a4b feat(01-01): create Next.js 15 project
|
||||
|
||||
# Initialization
|
||||
5c6d7e docs: initialize ecommerce-app (5 phases)
|
||||
```
|
||||
|
||||
每个计划产生 2-4 个提交(任务 + 元数据)。清晰、细粒度、可 bisect。
|
||||
|
||||
</example_log>
|
||||
|
||||
<anti_patterns>
|
||||
|
||||
**仍不要提交(中间产物):**
|
||||
- PLAN.md 创建(与计划完成一起提交)
|
||||
- RESEARCH.md(中间产物)
|
||||
- DISCOVERY.md(中间产物)
|
||||
- 小的规划调整
|
||||
- "Fixed typo in roadmap"
|
||||
|
||||
**要提交(结果):**
|
||||
- 每个任务完成(feat/fix/test/refactor)
|
||||
- 计划完成元数据(docs)
|
||||
- 项目初始化(docs)
|
||||
|
||||
**关键原则:** 提交可工作的代码和已发布的结果,而非规划过程。
|
||||
|
||||
</anti_patterns>
|
||||
|
||||
<commit_strategy_rationale>
|
||||
|
||||
## 为什么使用每任务提交?
|
||||
|
||||
**AI 上下文工程:**
|
||||
- Git 历史成为未来 Claude 会话的主要上下文源
|
||||
- `git log --grep="{phase}-{plan}"` 显示计划的所有工作
|
||||
- `git diff <hash>^..<hash>` 显示每个任务的确切变更
|
||||
- 减少对解析 SUMMARY.md 的依赖 = 更多上下文用于实际工作
|
||||
|
||||
**失败恢复:**
|
||||
- 任务 1 已提交 ✅,任务 2 失败 ❌
|
||||
- 下次会话中的 Claude:看到任务 1 完成,可以重试任务 2
|
||||
- 可以 `git reset --hard` 到最后一个成功的任务
|
||||
|
||||
**调试:**
|
||||
- `git bisect` 找到确切的失败任务,而不仅仅是失败计划
|
||||
- `git blame` 将行追溯到特定任务上下文
|
||||
- 每个提交独立可回滚
|
||||
|
||||
**可观察性:**
|
||||
- 独立开发者 + Claude 工作流受益于细粒度归因
|
||||
- 原子提交是 git 最佳实践
|
||||
- 当消费者是 Claude 而非人类时,"提交噪音"无关紧要
|
||||
|
||||
</commit_strategy_rationale>
|
||||
38
docs/zh-CN/references/git-planning-commit.md
Normal file
38
docs/zh-CN/references/git-planning-commit.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Git 规划提交
|
||||
|
||||
使用 gsd-tools CLI 提交规划工件,它会自动检查 `commit_docs` 配置和 gitignore 状态。
|
||||
|
||||
## 通过 CLI 提交
|
||||
|
||||
始终使用 `gsd-tools.cjs commit` 处理 `.planning/` 文件 — 它会自动处理 `commit_docs` 和 gitignore 检查:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
如果 `commit_docs` 为 `false` 或 `.planning/` 被 gitignore,CLI 会返回 `skipped`(带原因)。无需手动条件检查。
|
||||
|
||||
## 修改上次提交
|
||||
|
||||
将 `.planning/` 文件变更合并到上次提交:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "" --files .planning/codebase/*.md --amend
|
||||
```
|
||||
|
||||
## 提交消息模式
|
||||
|
||||
| 命令 | 范围 | 示例 |
|
||||
|------|------|------|
|
||||
| plan-phase | phase | `docs(phase-03): create authentication plans` |
|
||||
| execute-phase | phase | `docs(phase-03): complete authentication phase` |
|
||||
| new-milestone | milestone | `docs: start milestone v1.1` |
|
||||
| remove-phase | chore | `chore: remove phase 17 (dashboard)` |
|
||||
| insert-phase | phase | `docs: insert phase 16.1 (critical fix)` |
|
||||
| add-phase | phase | `docs: add phase 07 (settings page)` |
|
||||
|
||||
## 何时跳过
|
||||
|
||||
- config 中 `commit_docs: false`
|
||||
- `.planning/` 被 gitignore
|
||||
- 无变更可提交(用 `git status --porcelain .planning/` 检查)
|
||||
34
docs/zh-CN/references/model-profile-resolution.md
Normal file
34
docs/zh-CN/references/model-profile-resolution.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 模型配置解析
|
||||
|
||||
在编排开始时解析一次模型配置,然后在所有 Task 生成时使用。
|
||||
|
||||
## 解析模式
|
||||
|
||||
```bash
|
||||
MODEL_PROFILE=$(cat .planning/config.json 2>/dev/null | grep -o '"model_profile"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"' || echo "balanced")
|
||||
```
|
||||
|
||||
默认值:未设置或缺少 config 时为 `balanced`。
|
||||
|
||||
## 查找表
|
||||
|
||||
@~/.claude/get-shit-done/references/model-profiles.md
|
||||
|
||||
在表中查找已解析配置对应的代理。将 model 参数传递给 Task 调用:
|
||||
|
||||
```
|
||||
Task(
|
||||
prompt="...",
|
||||
subagent_type="gsd-planner",
|
||||
model="{resolved_model}" # "inherit"、"sonnet" 或 "haiku"
|
||||
)
|
||||
```
|
||||
|
||||
**注意:** Opus 级代理解析为 `"inherit"`(而非 `"opus"`)。这会使代理使用父会话的模型,避免与可能阻止特定 opus 版本的组织策略冲突。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 在编排开始时解析一次
|
||||
2. 存储 profile 值
|
||||
3. 生成时在表中查找每个代理的模型
|
||||
4. 将 model 参数传递给每个 Task 调用(值:`"inherit"`、`"sonnet"`、`"haiku"`)
|
||||
93
docs/zh-CN/references/model-profiles.md
Normal file
93
docs/zh-CN/references/model-profiles.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 模型配置
|
||||
|
||||
模型配置控制每个 GSD 代理使用哪个 Claude 模型。这允许平衡质量和 token 消耗。
|
||||
|
||||
## 配置定义
|
||||
|
||||
| 代理 | `quality` | `balanced` | `budget` |
|
||||
|-------|-----------|------------|----------|
|
||||
| gsd-planner | opus | opus | sonnet |
|
||||
| gsd-roadmapper | opus | sonnet | sonnet |
|
||||
| gsd-executor | opus | sonnet | sonnet |
|
||||
| gsd-phase-researcher | opus | sonnet | haiku |
|
||||
| gsd-project-researcher | opus | sonnet | haiku |
|
||||
| gsd-research-synthesizer | sonnet | sonnet | haiku |
|
||||
| gsd-debugger | opus | sonnet | sonnet |
|
||||
| gsd-codebase-mapper | sonnet | haiku | haiku |
|
||||
| gsd-verifier | sonnet | sonnet | haiku |
|
||||
| gsd-plan-checker | sonnet | sonnet | haiku |
|
||||
| gsd-integration-checker | sonnet | sonnet | haiku |
|
||||
| gsd-nyquist-auditor | sonnet | sonnet | haiku |
|
||||
|
||||
## 配置理念
|
||||
|
||||
**quality** - 最大推理能力
|
||||
- 所有决策代理使用 Opus
|
||||
- 只读验证使用 Sonnet
|
||||
- 适用场景:有配额可用、关键架构工作
|
||||
|
||||
**balanced**(默认)- 智能分配
|
||||
- 仅规划(架构决策发生的地方)使用 Opus
|
||||
- 执行和研究使用 Sonnet(遵循明确指令)
|
||||
- 验证使用 Sonnet(需要推理,不仅仅是模式匹配)
|
||||
- 适用场景:正常开发、质量与成本的良好平衡
|
||||
|
||||
**budget** - 最小化 Opus 使用
|
||||
- 编写代码的使用 Sonnet
|
||||
- 研究和验证使用 Haiku
|
||||
- 适用场景:节省配额、大量工作、不太关键的阶段
|
||||
|
||||
## 解析逻辑
|
||||
|
||||
编排器在生成代理前解析模型:
|
||||
|
||||
```
|
||||
1. 读取 .planning/config.json
|
||||
2. 检查 model_overrides 是否有代理特定覆盖
|
||||
3. 如果没有覆盖,在配置表中查找代理
|
||||
4. 将 model 参数传递给 Task 调用
|
||||
```
|
||||
|
||||
## 单代理覆盖
|
||||
|
||||
覆盖特定代理而不更改整个配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"model_profile": "balanced",
|
||||
"model_overrides": {
|
||||
"gsd-executor": "opus",
|
||||
"gsd-planner": "haiku"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
覆盖优先于配置。有效值:`opus`、`sonnet`、`haiku`。
|
||||
|
||||
## 切换配置
|
||||
|
||||
运行时:`/gsd:set-profile <profile>`
|
||||
|
||||
项目默认值:在 `.planning/config.json` 中设置:
|
||||
```json
|
||||
{
|
||||
"model_profile": "balanced"
|
||||
}
|
||||
```
|
||||
|
||||
## 设计理由
|
||||
|
||||
**为什么 gsd-planner 使用 Opus?**
|
||||
规划涉及架构决策、目标分解和任务设计。这是模型质量影响最大的地方。
|
||||
|
||||
**为什么 gsd-executor 使用 Sonnet?**
|
||||
执行者遵循明确的 PLAN.md 指令。计划已包含推理;执行只是实现。
|
||||
|
||||
**为什么 balanced 中验证器使用 Sonnet(而非 Haiku)?**
|
||||
验证需要目标回溯推理 —— 检查代码是否**交付**了阶段承诺的内容,而不仅仅是模式匹配。Sonnet 处理得很好;Haiku 可能会遗漏细微的差距。
|
||||
|
||||
**为什么 gsd-codebase-mapper 使用 Haiku?**
|
||||
只读探索和模式提取。不需要推理,只需从文件内容输出结构化结果。
|
||||
|
||||
**为什么用 `inherit` 而不是直接传递 `opus`?**
|
||||
Claude Code 的 `"opus"` 别名映射到特定模型版本。组织可能阻止旧版 opus 而允许新版。GSD 为 opus 级代理返回 `"inherit"`,使其使用用户在会话中配置的任何 opus 版本。这避免了版本冲突和静默回退到 Sonnet。
|
||||
61
docs/zh-CN/references/phase-argument-parsing.md
Normal file
61
docs/zh-CN/references/phase-argument-parsing.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 阶段参数解析
|
||||
|
||||
为操作阶段的命令解析和规范化阶段参数。
|
||||
|
||||
## 提取
|
||||
|
||||
从 `$ARGUMENTS` 中:
|
||||
- 提取阶段编号(第一个数字参数)
|
||||
- 提取标志(以 `--` 为前缀)
|
||||
- 剩余文本为描述(用于 insert/add 命令)
|
||||
|
||||
## 使用 gsd-tools
|
||||
|
||||
`find-phase` 命令一步完成规范化和验证:
|
||||
|
||||
```bash
|
||||
PHASE_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}")
|
||||
```
|
||||
|
||||
返回 JSON 包含:
|
||||
- `found`: true/false
|
||||
- `directory`: 阶段目录的完整路径
|
||||
- `phase_number`: 规范化的编号(如 "06"、"06.1")
|
||||
- `phase_name`: 名称部分(如 "foundation")
|
||||
- `plans`: PLAN.md 文件数组
|
||||
- `summaries`: SUMMARY.md 文件数组
|
||||
|
||||
## 手动规范化(遗留)
|
||||
|
||||
将整数阶段补零到 2 位。保留小数后缀。
|
||||
|
||||
```bash
|
||||
# 规范化阶段编号
|
||||
if [[ "$PHASE" =~ ^[0-9]+$ ]]; then
|
||||
# 整数: 8 → 08
|
||||
PHASE=$(printf "%02d" "$PHASE")
|
||||
elif [[ "$PHASE" =~ ^([0-9]+)\.([0-9]+)$ ]]; then
|
||||
# 小数: 2.1 → 02.1
|
||||
PHASE=$(printf "%02d.%s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}")
|
||||
fi
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
使用 `roadmap get-phase` 验证阶段存在:
|
||||
|
||||
```bash
|
||||
PHASE_CHECK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}")
|
||||
if [ "$(printf '%s\n' "$PHASE_CHECK" | jq -r '.found')" = "false" ]; then
|
||||
echo "ERROR: Phase ${PHASE} not found in roadmap"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## 目录查找
|
||||
|
||||
使用 `find-phase` 进行目录查找:
|
||||
|
||||
```bash
|
||||
PHASE_DIR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}" --raw)
|
||||
```
|
||||
200
docs/zh-CN/references/planning-config.md
Normal file
200
docs/zh-CN/references/planning-config.md
Normal file
@@ -0,0 +1,200 @@
|
||||
<planning_config>
|
||||
|
||||
`.planning/` 目录行为的配置选项。
|
||||
|
||||
<config_schema>
|
||||
```json
|
||||
"planning": {
|
||||
"commit_docs": true,
|
||||
"search_gitignored": false
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}"
|
||||
}
|
||||
```
|
||||
|
||||
| 选项 | 默认值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `commit_docs` | `true` | 是否将规划工件提交到 git |
|
||||
| `search_gitignored` | `false` | 在广泛 rg 搜索中添加 `--no-ignore` |
|
||||
| `git.branching_strategy` | `"none"` | Git 分支策略:`"none"`、`"phase"` 或 `"milestone"` |
|
||||
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | 阶段策略的分支模板 |
|
||||
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | 里程碑策略的分支模板 |
|
||||
</config_schema>
|
||||
|
||||
<commit_docs_behavior>
|
||||
|
||||
**当 `commit_docs: true`(默认):**
|
||||
- 规划文件正常提交
|
||||
- SUMMARY.md、STATE.md、ROADMAP.md 在 git 中跟踪
|
||||
- 规划决策的完整历史保留
|
||||
|
||||
**当 `commit_docs: false`:**
|
||||
- 跳过 `.planning/` 文件的所有 `git add`/`git commit`
|
||||
- 用户必须将 `.planning/` 添加到 `.gitignore`
|
||||
- 适用于:OSS 贡献、客户项目、保持规划私有
|
||||
|
||||
**使用 gsd-tools.cjs(推荐):**
|
||||
|
||||
```bash
|
||||
# 提交时自动检查 commit_docs + gitignore:
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md
|
||||
|
||||
# 通过 state load 加载配置(返回 JSON):
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# commit_docs 在 JSON 输出中可用
|
||||
|
||||
# 或使用包含 commit_docs 的 init 命令:
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# commit_docs 包含在所有 init 命令输出中
|
||||
```
|
||||
|
||||
**自动检测:** 如果 `.planning/` 被 gitignore,无论 config.json 如何,`commit_docs` 自动为 `false`。这防止用户在 `.gitignore` 中有 `.planning/` 时出现 git 错误。
|
||||
|
||||
**通过 CLI 提交(自动处理检查):**
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md
|
||||
```
|
||||
|
||||
CLI 在内部检查 `commit_docs` 配置和 gitignore 状态 —— 无需手动条件判断。
|
||||
|
||||
</commit_docs_behavior>
|
||||
|
||||
<search_behavior>
|
||||
|
||||
**当 `search_gitignored: false`(默认):**
|
||||
- 标准 rg 行为(尊重 .gitignore)
|
||||
- 直接路径搜索有效:`rg "pattern" .planning/` 找到文件
|
||||
- 广泛搜索跳过 gitignored:`rg "pattern"` 跳过 `.planning/`
|
||||
|
||||
**当 `search_gitignored: true`:**
|
||||
- 在应该包含 `.planning/` 的广泛 rg 搜索中添加 `--no-ignore`
|
||||
- 仅在搜索整个仓库并期望 `.planning/` 匹配时需要
|
||||
|
||||
**注意:** 大多数 GSD 操作使用直接文件读取或显式路径,无论 gitignore 状态如何都有效。
|
||||
|
||||
</search_behavior>
|
||||
|
||||
<setup_uncommitted_mode>
|
||||
|
||||
使用未提交模式:
|
||||
|
||||
1. **设置配置:**
|
||||
```json
|
||||
"planning": {
|
||||
"commit_docs": false,
|
||||
"search_gitignored": true
|
||||
}
|
||||
```
|
||||
|
||||
2. **添加到 .gitignore:**
|
||||
```
|
||||
.planning/
|
||||
```
|
||||
|
||||
3. **已存在的跟踪文件:** 如果 `.planning/` 之前被跟踪:
|
||||
```bash
|
||||
git rm -r --cached .planning/
|
||||
git commit -m "chore: stop tracking planning docs"
|
||||
```
|
||||
|
||||
4. **分支合并:** 当使用 `branching_strategy: phase` 或 `milestone` 时,`complete-milestone` 工作流在 `commit_docs: false` 时自动从暂存区移除 `.planning/` 文件,然后才进行合并提交。
|
||||
|
||||
</setup_uncommitted_mode>
|
||||
|
||||
<branching_strategy_behavior>
|
||||
|
||||
**分支策略:**
|
||||
|
||||
| 策略 | 创建分支时机 | 分支范围 | 合并点 |
|
||||
|----------|---------------------|--------------|-------------|
|
||||
| `none` | 从不 | N/A | N/A |
|
||||
| `phase` | `execute-phase` 开始时 | 单个阶段 | 阶段后用户手动合并 |
|
||||
| `milestone` | 里程碑第一个 `execute-phase` | 整个里程碑 | `complete-milestone` 时 |
|
||||
|
||||
**当 `git.branching_strategy: "none"`(默认):**
|
||||
- 所有工作提交到当前分支
|
||||
- 标准 GSD 行为
|
||||
|
||||
**当 `git.branching_strategy: "phase"`:**
|
||||
- `execute-phase` 在执行前创建/切换到分支
|
||||
- 分支名来自 `phase_branch_template`(如 `gsd/phase-03-authentication`)
|
||||
- 所有计划提交到该分支
|
||||
- 阶段完成后用户手动合并分支
|
||||
- `complete-milestone` 提供合并所有阶段分支的选项
|
||||
|
||||
**当 `git.branching_strategy: "milestone"`:**
|
||||
- 里程碑的第一个 `execute-phase` 创建里程碑分支
|
||||
- 分支名来自 `milestone_branch_template`(如 `gsd/v1.0-mvp`)
|
||||
- 里程碑中所有阶段提交到同一分支
|
||||
- `complete-milestone` 提供将里程碑分支合并到 main 的选项
|
||||
|
||||
**模板变量:**
|
||||
|
||||
| 变量 | 可用于 | 描述 |
|
||||
|----------|--------------|-------------|
|
||||
| `{phase}` | phase_branch_template | 零填充阶段号(如 "03") |
|
||||
| `{slug}` | 两者 | 小写、连字符名称 |
|
||||
| `{milestone}` | milestone_branch_template | 里程碑版本(如 "v1.0") |
|
||||
|
||||
**检查配置:**
|
||||
|
||||
使用 `init execute-phase` 返回所有配置为 JSON:
|
||||
```bash
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# JSON 输出包含:branching_strategy, phase_branch_template, milestone_branch_template
|
||||
```
|
||||
|
||||
或使用 `state load` 获取配置值:
|
||||
```bash
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# 从 JSON 解析 branching_strategy, phase_branch_template, milestone_branch_template
|
||||
```
|
||||
|
||||
**分支创建:**
|
||||
|
||||
```bash
|
||||
# 阶段策略
|
||||
if [ "$BRANCHING_STRATEGY" = "phase" ]; then
|
||||
PHASE_SLUG=$(echo "$PHASE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
BRANCH_NAME=$(echo "$PHASE_BRANCH_TEMPLATE" | sed "s/{phase}/$PADDED_PHASE/g" | sed "s/{slug}/$PHASE_SLUG/g")
|
||||
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
|
||||
fi
|
||||
|
||||
# 里程碑策略
|
||||
if [ "$BRANCHING_STRATEGY" = "milestone" ]; then
|
||||
MILESTONE_SLUG=$(echo "$MILESTONE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
BRANCH_NAME=$(echo "$MILESTONE_BRANCH_TEMPLATE" | sed "s/{milestone}/$MILESTONE_VERSION/g" | sed "s/{slug}/$MILESTONE_SLUG/g")
|
||||
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME"
|
||||
fi
|
||||
```
|
||||
|
||||
**complete-milestone 时的合并选项:**
|
||||
|
||||
| 选项 | Git 命令 | 结果 |
|
||||
|--------|-------------|--------|
|
||||
| Squash 合并(推荐) | `git merge --squash` | 每个分支单个干净提交 |
|
||||
| 带历史合并 | `git merge --no-ff` | 保留所有单独提交 |
|
||||
| 不合并直接删除 | `git branch -D` | 丢弃分支工作 |
|
||||
| 保留分支 | (无) | 后续手动处理 |
|
||||
|
||||
推荐 Squash 合并 —— 保持 main 分支历史干净,同时在分支中保留完整开发历史(直到删除)。
|
||||
|
||||
**使用场景:**
|
||||
|
||||
| 策略 | 最适合 |
|
||||
|----------|----------|
|
||||
| `none` | 独立开发、简单项目 |
|
||||
| `phase` | 每阶段代码审查、细粒度回滚、团队协作 |
|
||||
| `milestone` | 发布分支、预发布环境、每个版本一个 PR |
|
||||
|
||||
</branching_strategy_behavior>
|
||||
|
||||
</planning_config>
|
||||
142
docs/zh-CN/references/questioning.md
Normal file
142
docs/zh-CN/references/questioning.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 提问指南
|
||||
|
||||
项目初始化是梦想提取,而非需求收集。你在帮助用户发现和表达他们想构建的内容。这不是合同谈判 —— 是协作思考。
|
||||
|
||||
## 理念
|
||||
|
||||
**你是思考伙伴,不是面试官。**
|
||||
|
||||
用户通常有一个模糊的想法。你的工作是帮助他们将其锐化。问一些让他们思考"哦,我没想到那个"或"是的,这正是我的意思"的问题。
|
||||
|
||||
不要审问。协作。不要照本宣科。顺藤摸瓜。
|
||||
|
||||
## 目标
|
||||
|
||||
到提问结束时,你需要足够的清晰度来编写下游阶段可执行的 PROJECT.md:
|
||||
|
||||
- **研究** 需要:研究什么领域、用户已知什么、存在哪些未知
|
||||
- **需求** 需要:足够清晰的愿景来界定 v1 功能
|
||||
- **路线图** 需要:足够清晰的愿景来分解为阶段、"完成"是什么样子
|
||||
- **plan-phase** 需要:可分解为任务的具体需求、实现选择的上下文
|
||||
- **execute-phase** 需要:可验证的成功标准、需求背后的"为什么"
|
||||
|
||||
模糊的 PROJECT.md 会让每个下游阶段都在猜测。成本会叠加。
|
||||
|
||||
## 如何提问
|
||||
|
||||
**开放开始。** 让他们倾倒心理模型。不要用结构打断。
|
||||
|
||||
**跟随能量。** 无论他们强调什么,深入那个。什么让他们兴奋?什么问题引发了这一切?
|
||||
|
||||
**挑战模糊。** 绝不接受模糊回答。"好"意味着什么?"用户"指谁?"简单"是怎么简单?
|
||||
|
||||
**让抽象具体。**"带我走一遍使用这个。""那实际看起来是什么样?"
|
||||
|
||||
**澄清歧义。**"你说 Z 时,是指 A 还是 B?""你提到了 X —— 跟我多说说。"
|
||||
|
||||
**知道何时停止。** 当你理解他们想要什么、为什么想要、给谁用、完成是什么样 —— 提议继续。
|
||||
|
||||
## 问题类型
|
||||
|
||||
以此作为灵感,不是清单。选择与话题相关的。
|
||||
|
||||
**动机 —— 为什么存在:**
|
||||
- "什么引发了这一切?"
|
||||
- "你今天在做什么会被这个替代?"
|
||||
- "如果这个存在,你会做什么?"
|
||||
|
||||
**具体性 —— 它实际是什么:**
|
||||
- "带我走一遍使用这个"
|
||||
- "你说 X —— 那实际看起来是什么样?"
|
||||
- "给我一个例子"
|
||||
|
||||
**澄清 —— 他们什么意思:**
|
||||
- "你说 Z 时,是指 A 还是 B?"
|
||||
- "你提到了 X —— 跟我多说说那个"
|
||||
|
||||
**成功 —— 你怎么知道它在工作:**
|
||||
- "你怎么知道这个在工作?"
|
||||
- "完成是什么样子?"
|
||||
|
||||
## 使用 AskUserQuestion
|
||||
|
||||
用 AskUserQuestion 帮助用户思考,通过呈现具体的选项供他们反应。
|
||||
|
||||
**好选项:**
|
||||
- 他们可能意思的解读
|
||||
- 确认或否认的具体例子
|
||||
- 揭示优先级的具体选择
|
||||
|
||||
**坏选项:**
|
||||
- 泛泛的类别("技术"、"业务"、"其他")
|
||||
- 预设答案的引导性选项
|
||||
- 选项太多(2-4 个理想)
|
||||
- 超过 12 个字符的标题(硬限制 —— 验证会拒绝)
|
||||
|
||||
**示例 —— 模糊回答:**
|
||||
用户说"它应该快"
|
||||
|
||||
- header: "快"
|
||||
- question: "快是指?"
|
||||
- options: ["亚秒响应", "处理大数据集", "快速构建", "让我解释"]
|
||||
|
||||
**示例 —— 跟随话题:**
|
||||
用户提到"对当前工具感到沮丧"
|
||||
|
||||
- header: "沮丧"
|
||||
- question: "具体什么让你沮丧?"
|
||||
- options: ["点击太多", "缺少功能", "不可靠", "让我解释"]
|
||||
|
||||
**给用户的提示 —— 修改选项:**
|
||||
想要稍微修改某个选项版本的用户可以选择"Other"并通过编号引用选项:`#1 但仅用于指关节` 或 `#2 禁用分页`。这避免重新输入完整选项文本。
|
||||
|
||||
## 自由格式规则
|
||||
|
||||
**当用户想自由解释时,停止使用 AskUserQuestion。**
|
||||
|
||||
如果用户选择"Other"且他们的回应表明他们想用自己的话描述(如"让我描述一下"、"我来解释"、"别的"、或任何非选择/修改现有选项的开放式回复),你必须:
|
||||
|
||||
1. **用纯文本问你的追问** — 不通过 AskUserQuestion
|
||||
2. **等待他们在正常提示符下输入**
|
||||
3. **仅在处理他们的自由格式回应后恢复 AskUserQuestion**
|
||||
|
||||
同样适用于如果你包含一个表明自由格式的选项(如"让我解释"或"详细描述")且用户选择了它。
|
||||
|
||||
**错误:** 用户说"让我描述一下" → AskUserQuestion("什么功能?", ["功能 A", "功能 B", "详细描述"])
|
||||
**正确:** 用户说"让我描述一下" → "请讲 —— 你在想什么?"
|
||||
|
||||
## 上下文清单
|
||||
|
||||
以此作为**背景清单**,而非对话结构。进行时在脑中检查这些。如果还有缺口,自然地穿插问题。
|
||||
|
||||
- [ ] 他们在构建什么(足够具体可以向陌生人解释)
|
||||
- [ ] 为什么它需要存在(驱动它的问题或渴望)
|
||||
- [ ] 给谁用的(即使只是他们自己)
|
||||
- [ ] "完成"是什么样子(可观察的结果)
|
||||
|
||||
四件事。如果他们主动提供更多,捕获它。
|
||||
|
||||
## 决策门控
|
||||
|
||||
当你能写出清晰的 PROJECT.md 时,提议继续:
|
||||
|
||||
- header: "准备好了?"
|
||||
- question: "我想我理解你想要什么了。准备创建 PROJECT.md 吗?"
|
||||
- options:
|
||||
- "创建 PROJECT.md" — 让我们继续
|
||||
- "继续探索" — 我想分享更多 / 再问我
|
||||
|
||||
如果"继续探索" —— 问他们想添加什么或识别缺口并自然探查。
|
||||
|
||||
循环直到选择"创建 PROJECT.md"。
|
||||
|
||||
## 反模式
|
||||
|
||||
- **走清单** — 不管他们说什么都按领域走
|
||||
- **套话问题** — "你的核心价值是什么?""什么超出范围?"不管上下文
|
||||
- **企业腔** — "你的成功标准是什么?""你的利益相关者是谁?"
|
||||
- **审问** — 不基于回答构建就连续发问
|
||||
- **急于求成** — 最小化问题以开始"实际工作"
|
||||
- **浅层接受** — 不探查就接受模糊回答
|
||||
- **过早约束** — 还不理解想法就问技术栈
|
||||
- **用户技能** — 绝不问用户的技术经验。Claude 来构建。
|
||||
263
docs/zh-CN/references/tdd.md
Normal file
263
docs/zh-CN/references/tdd.md
Normal file
@@ -0,0 +1,263 @@
|
||||
<overview>
|
||||
TDD 关乎设计质量,而非覆盖率指标。红-绿-重构循环迫使你在实现前思考行为,从而产生更清晰的接口和更可测试的代码。
|
||||
|
||||
**原则:** 如果在编写 `fn` 之前能用 `expect(fn(input)).toBe(output)` 描述行为,TDD 会改善结果。
|
||||
|
||||
**关键洞察:** TDD 工作本质上比标准任务更重 —— 它需要 2-3 个执行周期(RED → GREEN → REFACTOR),每个周期都涉及文件读取、测试运行和可能的调试。TDD 功能获得专门的计划,以确保整个周期内有完整的上下文可用。
|
||||
</overview>
|
||||
|
||||
<when_to_use_tdd>
|
||||
## 何时 TDD 提高质量
|
||||
|
||||
**TDD 候选(创建 TDD 计划):**
|
||||
- 有明确输入/输出的业务逻辑
|
||||
- 有请求/响应契约的 API 端点
|
||||
- 数据转换、解析、格式化
|
||||
- 验证规则和约束
|
||||
- 有可测试行为的算法
|
||||
- 状态机和工作流
|
||||
- 有清晰规格的工具函数
|
||||
|
||||
**跳过 TDD(使用带 `type="auto"` 任务的标准计划):**
|
||||
- UI 布局、样式、视觉组件
|
||||
- 配置更改
|
||||
- 连接现有组件的胶水代码
|
||||
- 一次性脚本和迁移
|
||||
- 无业务逻辑的简单 CRUD
|
||||
- 探索性原型
|
||||
|
||||
**启发式:** 能在编写 `fn` 之前写 `expect(fn(input)).toBe(output)` 吗?
|
||||
→ 能:创建 TDD 计划
|
||||
→ 不能:使用标准计划,事后添加测试(如需要)
|
||||
</when_to_use_tdd>
|
||||
|
||||
<tdd_plan_structure>
|
||||
## TDD 计划结构
|
||||
|
||||
每个 TDD 计划通过完整的 RED-GREEN-REFACTOR 循环实现**一个功能**。
|
||||
|
||||
```markdown
|
||||
---
|
||||
phase: XX-name
|
||||
plan: NN
|
||||
type: tdd
|
||||
---
|
||||
|
||||
<objective>
|
||||
[什么功能以及为什么]
|
||||
Purpose: [该功能 TDD 的设计收益]
|
||||
Output: [可工作的、已测试的功能]
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@relevant/source/files.ts
|
||||
</context>
|
||||
|
||||
<feature>
|
||||
<name>[功能名称]</name>
|
||||
<files>[源文件, 测试文件]</files>
|
||||
<behavior>
|
||||
[可测试术语描述的预期行为]
|
||||
Cases: 输入 → 预期输出
|
||||
</behavior>
|
||||
<implementation>[测试通过后如何实现]</implementation>
|
||||
</feature>
|
||||
|
||||
<verification>
|
||||
[证明功能有效的测试命令]
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 失败测试已编写并提交
|
||||
- 实现通过测试
|
||||
- 重构完成(如需要)
|
||||
- 所有 2-3 个提交都存在
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后,创建包含以下内容的 SUMMARY.md:
|
||||
- RED: 编写了什么测试,为什么失败
|
||||
- GREEN: 什么实现让它通过
|
||||
- REFACTOR: 做了什么清理(如有)
|
||||
- Commits: 生成的提交列表
|
||||
</output>
|
||||
```
|
||||
|
||||
**每个 TDD 计划一个功能。** 如果功能足够简单可以批量处理,那就足够简单可以跳过 TDD —— 使用标准计划,事后添加测试。
|
||||
</tdd_plan_structure>
|
||||
|
||||
<execution_flow>
|
||||
## 红-绿-重构循环
|
||||
|
||||
**RED - 编写失败测试:**
|
||||
1. 按项目约定创建测试文件
|
||||
2. 编写描述预期行为的测试(来自 `<behavior>` 元素)
|
||||
3. 运行测试 - 必须**失败**
|
||||
4. 如果测试通过:功能已存在或测试有误。调查。
|
||||
5. 提交:`test({phase}-{plan}): add failing test for [feature]`
|
||||
|
||||
**GREEN - 实现使其通过:**
|
||||
1. 编写使测试通过的最小代码
|
||||
2. 不耍小聪明,不优化 - 只让它工作
|
||||
3. 运行测试 - 必须**通过**
|
||||
4. 提交:`feat({phase}-{plan}): implement [feature]`
|
||||
|
||||
**REFACTOR(如需要):**
|
||||
1. 如果存在明显的改进,清理实现
|
||||
2. 运行测试 - 必须**仍然通过**
|
||||
3. 仅在做出更改时提交:`refactor({phase}-{plan}): clean up [feature]`
|
||||
|
||||
**结果:** 每个 TDD 计划产生 2-3 个原子提交。
|
||||
</execution_flow>
|
||||
|
||||
<test_quality>
|
||||
## 好测试 vs 坏测试
|
||||
|
||||
**测试行为,而非实现:**
|
||||
- 好:"返回格式化的日期字符串"
|
||||
- 坏:"用正确参数调用 formatDate 辅助函数"
|
||||
- 测试应该能经受重构
|
||||
|
||||
**每个测试一个概念:**
|
||||
- 好:分别为有效输入、空输入、畸形输入编写测试
|
||||
- 坏:用多个断言检查所有边缘情况的单个测试
|
||||
|
||||
**描述性名称:**
|
||||
- 好:"should reject empty email"、"returns null for invalid ID"
|
||||
- 坏:"test1"、"handles error"、"works correctly"
|
||||
|
||||
**不包含实现细节:**
|
||||
- 好:测试公共 API、可观察行为
|
||||
- 坏:Mock 内部实现、测试私有方法、断言内部状态
|
||||
</test_quality>
|
||||
|
||||
<framework_setup>
|
||||
## 测试框架设置(如不存在)
|
||||
|
||||
当执行 TDD 计划但没有配置测试框架时,作为 RED 阶段的一部分进行设置:
|
||||
|
||||
**1. 检测项目类型:**
|
||||
```bash
|
||||
# JavaScript/TypeScript
|
||||
if [ -f package.json ]; then echo "node"; fi
|
||||
|
||||
# Python
|
||||
if [ -f requirements.txt ] || [ -f pyproject.toml ]; then echo "python"; fi
|
||||
|
||||
# Go
|
||||
if [ -f go.mod ]; then echo "go"; fi
|
||||
|
||||
# Rust
|
||||
if [ -f Cargo.toml ]; then echo "rust"; fi
|
||||
```
|
||||
|
||||
**2. 安装最小框架:**
|
||||
| 项目 | 框架 | 安装 |
|
||||
|---------|-----------|---------|
|
||||
| Node.js | Jest | `npm install -D jest @types/jest ts-jest` |
|
||||
| Node.js (Vite) | Vitest | `npm install -D vitest` |
|
||||
| Python | pytest | `pip install pytest` |
|
||||
| Go | testing | 内置 |
|
||||
| Rust | cargo test | 内置 |
|
||||
|
||||
**3. 按需创建配置:**
|
||||
- Jest: 带 ts-jest preset 的 `jest.config.js`
|
||||
- Vitest: 带测试全局变量的 `vitest.config.ts`
|
||||
- pytest: `pytest.ini` 或 `pyproject.toml` 部分
|
||||
|
||||
**4. 验证设置:**
|
||||
```bash
|
||||
# 运行空测试套件 - 应该以 0 个测试通过
|
||||
npm test # Node
|
||||
pytest # Python
|
||||
go test ./... # Go
|
||||
cargo test # Rust
|
||||
```
|
||||
|
||||
**5. 创建第一个测试文件:**
|
||||
遵循项目约定的测试位置:
|
||||
- 源文件旁边的 `*.test.ts` / `*.spec.ts`
|
||||
- `__tests__/` 目录
|
||||
- 根目录的 `tests/` 目录
|
||||
|
||||
框架设置是第一个 TDD 计划 RED 阶段的一次性成本。
|
||||
</framework_setup>
|
||||
|
||||
<error_handling>
|
||||
## 错误处理
|
||||
|
||||
**测试在 RED 阶段没有失败:**
|
||||
- 功能可能已存在 - 调查
|
||||
- 测试可能有误(没测试你以为的东西)
|
||||
- 前进前修复
|
||||
|
||||
**测试在 GREEN 阶段没有通过:**
|
||||
- 调试实现
|
||||
- 不要跳到重构
|
||||
- 持续迭代直到绿色
|
||||
|
||||
**测试在 REFACTOR 阶段失败:**
|
||||
- 撤销重构
|
||||
- 提交过早
|
||||
- 用更小的步骤重构
|
||||
|
||||
**不相关的测试失败:**
|
||||
- 停下来调查
|
||||
- 可能表明耦合问题
|
||||
- 前进前修复
|
||||
</error_handling>
|
||||
|
||||
<commit_pattern>
|
||||
## TDD 计划的提交模式
|
||||
|
||||
TDD 计划产生 2-3 个原子提交(每个阶段一个):
|
||||
|
||||
```
|
||||
test(08-02): add failing test for email validation
|
||||
|
||||
- Tests valid email formats accepted
|
||||
- Tests invalid formats rejected
|
||||
- Tests empty input handling
|
||||
|
||||
feat(08-02): implement email validation
|
||||
|
||||
- Regex pattern matches RFC 5322
|
||||
- Returns boolean for validity
|
||||
- Handles edge cases (empty, null)
|
||||
|
||||
refactor(08-02): extract regex to constant (optional)
|
||||
|
||||
- Moved pattern to EMAIL_REGEX constant
|
||||
- No behavior changes
|
||||
- Tests still pass
|
||||
```
|
||||
|
||||
**与标准计划对比:**
|
||||
- 标准计划:每个任务 1 个提交,每个计划 2-4 个提交
|
||||
- TDD 计划:单个功能 2-3 个提交
|
||||
|
||||
两者遵循相同格式:`{type}({phase}-{plan}): {description}`
|
||||
|
||||
**好处:**
|
||||
- 每个提交独立可回滚
|
||||
- Git bisect 在提交级别工作
|
||||
- 显示 TDD 纪律的清晰历史
|
||||
- 与整体提交策略一致
|
||||
</commit_pattern>
|
||||
|
||||
<context_budget>
|
||||
## 上下文预算
|
||||
|
||||
TDD 计划目标 **~40% 上下文使用率**(低于标准计划的 ~50%)。
|
||||
|
||||
为什么更低:
|
||||
- RED 阶段:编写测试、运行测试、可能调试为什么没有失败
|
||||
- GREEN 阶段:实现、运行测试、可能对失败进行迭代
|
||||
- REFACTOR 阶段:修改代码、运行测试、验证无回归
|
||||
|
||||
每个阶段涉及读取文件、运行命令、分析输出。来回往复本质上比线性任务执行更重。
|
||||
|
||||
单一功能聚焦确保整个周期保持完整质量。
|
||||
</context_budget>
|
||||
158
docs/zh-CN/references/ui-brand.md
Normal file
158
docs/zh-CN/references/ui-brand.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# UI 品牌规范
|
||||
|
||||
面向用户的 GSD 输出的视觉模式。编排器通过 @ 引用此文件。
|
||||
|
||||
## 阶段横幅
|
||||
|
||||
用于主要工作流过渡。
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► {阶段名称}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
**阶段名称(大写):**
|
||||
- `QUESTIONING`(提问)
|
||||
- `RESEARCHING`(研究)
|
||||
- `DEFINING REQUIREMENTS`(定义需求)
|
||||
- `CREATING ROADMAP`(创建路线图)
|
||||
- `PLANNING PHASE {N}`(规划阶段 {N})
|
||||
- `EXECUTING WAVE {N}`(执行波次 {N})
|
||||
- `VERIFYING`(验证)
|
||||
- `PHASE {N} COMPLETE ✓`(阶段 {N} 完成)
|
||||
- `MILESTONE COMPLETE 🎉`(里程碑完成)
|
||||
|
||||
---
|
||||
|
||||
## 检查点框
|
||||
|
||||
需要用户操作。62 字符宽度。
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: {类型} ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
{内容}
|
||||
|
||||
──────────────────────────────────────────────────────────────
|
||||
→ {操作提示}
|
||||
──────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
**类型:**
|
||||
- `CHECKPOINT: 需要验证` → `→ 输入 "approved" 或描述问题`
|
||||
- `CHECKPOINT: 需要决策` → `→ 选择: option-a / option-b`
|
||||
- `CHECKPOINT: 需要操作` → `→ 完成后输入 "done"`
|
||||
|
||||
---
|
||||
|
||||
## 状态符号
|
||||
|
||||
```
|
||||
✓ 完成 / 通过 / 已验证
|
||||
✗ 失败 / 缺失 / 阻塞
|
||||
◆ 进行中
|
||||
○ 待处理
|
||||
⚡ 自动批准
|
||||
⚠ 警告
|
||||
🎉 里程碑完成(仅在横幅中)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 进度显示
|
||||
|
||||
**阶段/里程碑级别:**
|
||||
```
|
||||
进度: ████████░░ 80%
|
||||
```
|
||||
|
||||
**任务级别:**
|
||||
```
|
||||
任务: 2/4 完成
|
||||
```
|
||||
|
||||
**计划级别:**
|
||||
```
|
||||
计划: 3/5 完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生成指示器
|
||||
|
||||
```
|
||||
◆ 正在生成研究员...
|
||||
|
||||
◆ 并行生成 4 个研究员...
|
||||
→ 技术栈研究
|
||||
→ 功能研究
|
||||
→ 架构研究
|
||||
→ 陷阱研究
|
||||
|
||||
✓ 研究员完成: STACK.md 已写入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步区块
|
||||
|
||||
始终在主要完成后。
|
||||
|
||||
```
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
## ▶ 下一步
|
||||
|
||||
**{标识符}: {名称}** — {单行描述}
|
||||
|
||||
`{可复制粘贴的命令}`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
**也可选:**
|
||||
- `/gsd:alternative-1` — 描述
|
||||
- `/gsd:alternative-2` — 描述
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误框
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ERROR ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
{错误描述}
|
||||
|
||||
**修复方法:** {解决步骤}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 表格
|
||||
|
||||
```
|
||||
| 阶段 | 状态 | 计划 | 进度 |
|
||||
|------|------|------|------|
|
||||
| 1 | ✓ | 3/3 | 100% |
|
||||
| 2 | ◆ | 1/4 | 25% |
|
||||
| 3 | ○ | 0/2 | 0% |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反模式
|
||||
|
||||
- 变化的框/横幅宽度
|
||||
- 混合横幅样式(`===`、`---`、`***`)
|
||||
- 横幅中缺少 `GSD ►` 前缀
|
||||
- 随机 emoji(`🚀`、`✨`、`💫`)
|
||||
- 完成后缺少下一步区块
|
||||
612
docs/zh-CN/references/verification-patterns.md
Normal file
612
docs/zh-CN/references/verification-patterns.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# 验证模式
|
||||
|
||||
如何验证不同类型的工件是真实实现,而非存根或占位符。
|
||||
|
||||
<core_principle>
|
||||
**存在 ≠ 实现**
|
||||
|
||||
文件存在并不意味着功能有效。验证必须检查:
|
||||
1. **存在** - 文件在预期路径
|
||||
2. **实质性** - 内容是真实实现,非占位符
|
||||
3. **已连接** - 已连接到系统的其他部分
|
||||
4. **功能性** - 调用时实际工作
|
||||
|
||||
级别 1-3 可以编程检查。级别 4 通常需要人工验证。
|
||||
</core_principle>
|
||||
|
||||
<stub_detection>
|
||||
|
||||
## 通用存根模式
|
||||
|
||||
这些模式表明占位符代码,无论文件类型:
|
||||
|
||||
**基于注释的存根:**
|
||||
```bash
|
||||
# 存根注释的 Grep 模式
|
||||
grep -E "(TODO|FIXME|XXX|HACK|PLACEHOLDER)" "$file"
|
||||
grep -E "implement|add later|coming soon|will be" "$file" -i
|
||||
grep -E "// \.\.\.|/\* \.\.\. \*/|# \.\.\." "$file"
|
||||
```
|
||||
|
||||
**输出中的占位符文本:**
|
||||
```bash
|
||||
# UI 占位符模式
|
||||
grep -E "placeholder|lorem ipsum|coming soon|under construction" "$file" -i
|
||||
grep -E "sample|example|test data|dummy" "$file" -i
|
||||
grep -E "\[.*\]|<.*>|\{.*\}" "$file" # 模板括号未移除
|
||||
```
|
||||
|
||||
**空或琐碎实现:**
|
||||
```bash
|
||||
# 什么都不做的函数
|
||||
grep -E "return null|return undefined|return \{\}|return \[\]" "$file"
|
||||
grep -E "pass$|\.\.\.|\bnothing\b" "$file"
|
||||
grep -E "console\.(log|warn|error).*only" "$file" # 仅日志函数
|
||||
```
|
||||
|
||||
**预期动态但硬编码的值:**
|
||||
```bash
|
||||
# 硬编码 ID、计数或内容
|
||||
grep -E "id.*=.*['\"].*['\"]" "$file" # 硬编码字符串 ID
|
||||
grep -E "count.*=.*\d+|length.*=.*\d+" "$file" # 硬编码计数
|
||||
grep -E "\\\$\d+\.\d{2}|\d+ items" "$file" # 硬编码显示值
|
||||
```
|
||||
|
||||
</stub_detection>
|
||||
|
||||
<react_components>
|
||||
|
||||
## React/Next.js 组件
|
||||
|
||||
**存在检查:**
|
||||
```bash
|
||||
# 文件存在且导出组件
|
||||
[ -f "$component_path" ] && grep -E "export (default |)function|export const.*=.*\(" "$component_path"
|
||||
```
|
||||
|
||||
**实质性检查:**
|
||||
```bash
|
||||
# 返回实际 JSX,非占位符
|
||||
grep -E "return.*<" "$component_path" | grep -v "return.*null" | grep -v "placeholder" -i
|
||||
|
||||
# 有有意义的内容(不仅仅是包装 div)
|
||||
grep -E "<[A-Z][a-zA-Z]+|className=|onClick=|onChange=" "$component_path"
|
||||
|
||||
# 使用 props 或 state(非静态)
|
||||
grep -E "props\.|useState|useEffect|useContext|\{.*\}" "$component_path"
|
||||
```
|
||||
|
||||
**React 特有的存根模式:**
|
||||
```javascript
|
||||
// 危险信号 - 这些是存根:
|
||||
return <div>Component</div>
|
||||
return <div>Placeholder</div>
|
||||
return <div>{/* TODO */}</div>
|
||||
return <p>Coming soon</p>
|
||||
return null
|
||||
return <></>
|
||||
|
||||
// 也是存根 - 空处理器:
|
||||
onClick={() => {}}
|
||||
onChange={() => console.log('clicked')}
|
||||
onSubmit={(e) => e.preventDefault()} // 仅阻止默认,什么都不做
|
||||
```
|
||||
|
||||
**连接检查:**
|
||||
```bash
|
||||
# 组件导入它需要的东西
|
||||
grep -E "^import.*from" "$component_path"
|
||||
|
||||
# Props 实际被使用(不仅仅是接收)
|
||||
# 查找解构或 props.X 用法
|
||||
grep -E "\{ .* \}.*props|\bprops\.[a-zA-Z]+" "$component_path"
|
||||
|
||||
# API 调用存在(对于数据获取组件)
|
||||
grep -E "fetch\(|axios\.|useSWR|useQuery|getServerSideProps|getStaticProps" "$component_path"
|
||||
```
|
||||
|
||||
**功能验证(需要人工):**
|
||||
- 组件是否渲染可见内容?
|
||||
- 交互元素是否响应点击?
|
||||
- 数据是否加载并显示?
|
||||
- 错误状态是否适当显示?
|
||||
|
||||
</react_components>
|
||||
|
||||
<api_routes>
|
||||
|
||||
## API 路由(Next.js App Router / Express 等)
|
||||
|
||||
**存在检查:**
|
||||
```bash
|
||||
# 路由文件存在
|
||||
[ -f "$route_path" ]
|
||||
|
||||
# 导出 HTTP 方法处理器(Next.js App Router)
|
||||
grep -E "export (async )?(function|const) (GET|POST|PUT|PATCH|DELETE)" "$route_path"
|
||||
|
||||
# 或 Express 风格处理器
|
||||
grep -E "\.(get|post|put|patch|delete)\(" "$route_path"
|
||||
```
|
||||
|
||||
**实质性检查:**
|
||||
```bash
|
||||
# 有实际逻辑,不仅仅是 return 语句
|
||||
wc -l "$route_path" # 超过 10-15 行表明真实实现
|
||||
|
||||
# 与数据源交互
|
||||
grep -E "prisma\.|db\.|mongoose\.|sql|query|find|create|update|delete" "$route_path" -i
|
||||
|
||||
# 有错误处理
|
||||
grep -E "try|catch|throw|error|Error" "$route_path"
|
||||
|
||||
# 返回有意义的响应
|
||||
grep -E "Response\.json|res\.json|res\.send|return.*\{" "$route_path" | grep -v "message.*not implemented" -i
|
||||
```
|
||||
|
||||
**API 路由特有的存根模式:**
|
||||
```typescript
|
||||
// 危险信号 - 这些是存根:
|
||||
export async function POST() {
|
||||
return Response.json({ message: "Not implemented" })
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json([]) // 空 array 无数据库查询
|
||||
}
|
||||
|
||||
export async function PUT() {
|
||||
return new Response() // 空响应
|
||||
}
|
||||
|
||||
// 仅控制台日志:
|
||||
export async function POST(req) {
|
||||
console.log(await req.json())
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
```
|
||||
|
||||
**连接检查:**
|
||||
```bash
|
||||
# 导入数据库/服务客户端
|
||||
grep -E "^import.*prisma|^import.*db|^import.*client" "$route_path"
|
||||
|
||||
# 实际使用请求体(对于 POST/PUT)
|
||||
grep -E "req\.json\(\)|req\.body|request\.json\(\)" "$route_path"
|
||||
|
||||
# 验证输入(不仅仅信任请求)
|
||||
grep -E "schema\.parse|validate|zod|yup|joi" "$route_path"
|
||||
```
|
||||
|
||||
**功能验证(人工或自动化):**
|
||||
- GET 是否从数据库返回真实数据?
|
||||
- POST 是否实际创建记录?
|
||||
- 错误响应是否有正确的状态码?
|
||||
- 认证检查是否实际执行?
|
||||
|
||||
</api_routes>
|
||||
|
||||
<database_schema>
|
||||
|
||||
## 数据库模式(Prisma / Drizzle / SQL)
|
||||
|
||||
**存在检查:**
|
||||
```bash
|
||||
# 模式文件存在
|
||||
[ -f "prisma/schema.prisma" ] || [ -f "drizzle/schema.ts" ] || [ -f "src/db/schema.sql" ]
|
||||
|
||||
# 模型/表已定义
|
||||
grep -E "^model $model_name|CREATE TABLE $table_name|export const $table_name" "$schema_path"
|
||||
```
|
||||
|
||||
**实质性检查:**
|
||||
```bash
|
||||
# 有预期字段(不仅仅是 id)
|
||||
grep -A 20 "model $model_name" "$schema_path" | grep -E "^\s+\w+\s+\w+"
|
||||
|
||||
# 有预期关系
|
||||
grep -E "@relation|REFERENCES|FOREIGN KEY" "$schema_path"
|
||||
|
||||
# 有适当的字段类型(不全是 String)
|
||||
grep -A 20 "model $model_name" "$schema_path" | grep -E "Int|DateTime|Boolean|Float|Decimal|Json"
|
||||
```
|
||||
|
||||
**模式特有的存根模式:**
|
||||
```prisma
|
||||
// 危险信号 - 这些是存根:
|
||||
model User {
|
||||
id String @id
|
||||
// TODO: add fields
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id
|
||||
content String // 只有一个真实字段
|
||||
}
|
||||
|
||||
// 缺少关键字段:
|
||||
model Order {
|
||||
id String @id
|
||||
// 缺少: userId, items, total, status, createdAt
|
||||
}
|
||||
```
|
||||
|
||||
**连接检查:**
|
||||
```bash
|
||||
# 迁移存在且已应用
|
||||
ls prisma/migrations/ 2>/dev/null | wc -l # 应该 > 0
|
||||
npx prisma migrate status 2>/dev/null | grep -v "pending"
|
||||
|
||||
# 客户端已生成
|
||||
[ -d "node_modules/.prisma/client" ]
|
||||
```
|
||||
|
||||
**功能验证:**
|
||||
```bash
|
||||
# 可以查询表(自动化)
|
||||
npx prisma db execute --stdin <<< "SELECT COUNT(*) FROM $table_name"
|
||||
```
|
||||
|
||||
</database_schema>
|
||||
|
||||
<hooks_utilities>
|
||||
|
||||
## 自定义 Hooks 和工具
|
||||
|
||||
**存在检查:**
|
||||
```bash
|
||||
# 文件存在且导出函数
|
||||
[ -f "$hook_path" ] && grep -E "export (default )?(function|const)" "$hook_path"
|
||||
```
|
||||
|
||||
**实质性检查:**
|
||||
```bash
|
||||
# Hook 使用 React hooks(对于自定义 hooks)
|
||||
grep -E "useState|useEffect|useCallback|useMemo|useRef|useContext" "$hook_path"
|
||||
|
||||
# 有有意义的返回值
|
||||
grep -E "return \{|return \[" "$hook_path"
|
||||
|
||||
# 超过琐碎长度
|
||||
[ $(wc -l < "$hook_path") -gt 10 ]
|
||||
```
|
||||
|
||||
**Hooks 特有的存根模式:**
|
||||
```typescript
|
||||
// 危险信号 - 这些是存根:
|
||||
export function useAuth() {
|
||||
return { user: null, login: () => {}, logout: () => {} }
|
||||
}
|
||||
|
||||
export function useCart() {
|
||||
const [items, setItems] = useState([])
|
||||
return { items, addItem: () => console.log('add'), removeItem: () => {} }
|
||||
}
|
||||
|
||||
// 硬编码返回:
|
||||
export function useUser() {
|
||||
return { name: "Test User", email: "test@example.com" }
|
||||
}
|
||||
```
|
||||
|
||||
**连接检查:**
|
||||
```bash
|
||||
# Hook 实际在某处被导入
|
||||
grep -r "import.*$hook_name" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
|
||||
|
||||
# Hook 实际被调用
|
||||
grep -r "$hook_name()" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
|
||||
```
|
||||
|
||||
</hooks_utilities>
|
||||
|
||||
<environment_config>
|
||||
|
||||
## 环境变量和配置
|
||||
|
||||
**存在检查:**
|
||||
```bash
|
||||
# .env 文件存在
|
||||
[ -f ".env" ] || [ -f ".env.local" ]
|
||||
|
||||
# 必需变量已定义
|
||||
grep -E "^$VAR_NAME=" .env .env.local 2>/dev/null
|
||||
```
|
||||
|
||||
**实质性检查:**
|
||||
```bash
|
||||
# 变量有实际值(非占位符)
|
||||
grep -E "^$VAR_NAME=.+" .env .env.local 2>/dev/null | grep -v "your-.*-here|xxx|placeholder|TODO" -i
|
||||
|
||||
# 值对类型看起来有效:
|
||||
# - URL 应以 http 开头
|
||||
# - 密钥应足够长
|
||||
# - 布尔值应为 true/false
|
||||
```
|
||||
|
||||
**环境变量特有的存根模式:**
|
||||
```bash
|
||||
# 危险信号 - 这些是存根:
|
||||
DATABASE_URL=your-database-url-here
|
||||
STRIPE_SECRET_KEY=sk_test_xxx
|
||||
API_KEY=placeholder
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000 # 生产环境仍指向 localhost
|
||||
```
|
||||
|
||||
**连接检查:**
|
||||
```bash
|
||||
# 变量实际在代码中使用
|
||||
grep -r "process\.env\.$VAR_NAME|env\.$VAR_NAME" src/ --include="*.ts" --include="*.tsx"
|
||||
|
||||
# 变量在验证模式中(如果使用 zod 等验证 env)
|
||||
grep -E "$VAR_NAME" src/env.ts src/env.mjs 2>/dev/null
|
||||
```
|
||||
|
||||
</environment_config>
|
||||
|
||||
<wiring_verification>
|
||||
|
||||
## 连接验证模式
|
||||
|
||||
连接验证检查组件是否实际通信。这是大多数存根隐藏的地方。
|
||||
|
||||
### 模式:组件 → API
|
||||
|
||||
**检查:** 组件是否实际调用 API?
|
||||
|
||||
```bash
|
||||
# 查找 fetch/axios 调用
|
||||
grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component_path"
|
||||
|
||||
# 验证未被注释掉
|
||||
grep -E "fetch\(|axios\." "$component_path" | grep -v "^.*//.*fetch"
|
||||
|
||||
# 检查响应被使用
|
||||
grep -E "await.*fetch|\.then\(|setData|setState" "$component_path"
|
||||
```
|
||||
|
||||
**危险信号:**
|
||||
```typescript
|
||||
// Fetch 存在但响应被忽略:
|
||||
fetch('/api/messages') // 无 await,无 .then,无赋值
|
||||
|
||||
// Fetch 在注释中:
|
||||
// fetch('/api/messages').then(r => r.json()).then(setMessages)
|
||||
|
||||
// Fetch 到错误的端点:
|
||||
fetch('/api/message') // 拼写错误 - 应该是 /api/messages
|
||||
```
|
||||
|
||||
### 模式:API → 数据库
|
||||
|
||||
**检查:** API 路由是否实际查询数据库?
|
||||
|
||||
```bash
|
||||
# 查找数据库调用
|
||||
grep -E "prisma\.$model|db\.query|Model\.find" "$route_path"
|
||||
|
||||
# 验证被 await
|
||||
grep -E "await.*prisma|await.*db\." "$route_path"
|
||||
|
||||
# 检查结果被返回
|
||||
grep -E "return.*json.*data|res\.json.*result" "$route_path"
|
||||
```
|
||||
|
||||
**危险信号:**
|
||||
```typescript
|
||||
// 查询存在但结果未返回:
|
||||
await prisma.message.findMany()
|
||||
return Response.json({ ok: true }) // 返回静态值,非查询结果
|
||||
|
||||
// 查询未被 await:
|
||||
const messages = prisma.message.findMany() // 缺少 await
|
||||
return Response.json(messages) // 返回 Promise,非数据
|
||||
```
|
||||
|
||||
### 模式:表单 → 处理器
|
||||
|
||||
**检查:** 表单提交是否实际做些什么?
|
||||
|
||||
```bash
|
||||
# 查找 onSubmit 处理器
|
||||
grep -E "onSubmit=\{|handleSubmit" "$component_path"
|
||||
|
||||
# 检查处理器有内容
|
||||
grep -A 10 "onSubmit.*=" "$component_path" | grep -E "fetch|axios|mutate|dispatch"
|
||||
|
||||
# 验证不仅仅是 preventDefault
|
||||
grep -A 5 "onSubmit" "$component_path" | grep -v "only.*preventDefault" -i
|
||||
```
|
||||
|
||||
**危险信号:**
|
||||
```typescript
|
||||
// 处理器仅阻止默认:
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
|
||||
// 处理器仅日志:
|
||||
const handleSubmit = (data) => {
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
// 处理器为空:
|
||||
onSubmit={() => {}}
|
||||
```
|
||||
|
||||
### 模式:状态 → 渲染
|
||||
|
||||
**检查:** 组件是否渲染状态,而非硬编码内容?
|
||||
|
||||
```bash
|
||||
# 查找 JSX 中的状态使用
|
||||
grep -E "\{.*messages.*\}|\{.*data.*\}|\{.*items.*\}" "$component_path"
|
||||
|
||||
# 检查状态的 map/render
|
||||
grep -E "\.map\(|\.filter\(|\.reduce\(" "$component_path"
|
||||
|
||||
# 验证动态内容
|
||||
grep -E "\{[a-zA-Z_]+\." "$component_path" # 变量插值
|
||||
```
|
||||
|
||||
**危险信号:**
|
||||
```tsx
|
||||
// 硬编码而非状态:
|
||||
return <div>
|
||||
<p>Message 1</p>
|
||||
<p>Message 2</p>
|
||||
</div>
|
||||
|
||||
// 状态存在但未渲染:
|
||||
const [messages, setMessages] = useState([])
|
||||
return <div>No messages</div> // 总是显示 "no messages"
|
||||
|
||||
// 渲染错误的状态:
|
||||
const [messages, setMessages] = useState([])
|
||||
return <div>{otherData.map(...)}</div> // 使用不同数据
|
||||
```
|
||||
|
||||
</wiring_verification>
|
||||
|
||||
<verification_checklist>
|
||||
|
||||
## 快速验证清单
|
||||
|
||||
对于每种工件类型,运行此清单:
|
||||
|
||||
### 组件清单
|
||||
- [ ] 文件存在于预期路径
|
||||
- [ ] 导出函数/const 组件
|
||||
- [ ] 返回 JSX(非 null/空)
|
||||
- [ ] 渲染中无占位符文本
|
||||
- [ ] 使用 props 或 state(非静态)
|
||||
- [ ] 事件处理器有真实实现
|
||||
- [ ] 导入正确解析
|
||||
- [ ] 在应用某处被使用
|
||||
|
||||
### API 路由清单
|
||||
- [ ] 文件存在于预期路径
|
||||
- [ ] 导出 HTTP 方法处理器
|
||||
- [ ] 处理器超过 5 行
|
||||
- [ ] 查询数据库或服务
|
||||
- [ ] 返回有意义的响应(非空/占位符)
|
||||
- [ ] 有错误处理
|
||||
- [ ] 验证输入
|
||||
- [ ] 从前端调用
|
||||
|
||||
### 模式清单
|
||||
- [ ] 模型/表已定义
|
||||
- [ ] 有所有预期字段
|
||||
- [ ] 字段有适当类型
|
||||
- [ ] 如需要关系已定义
|
||||
- [ ] 迁移存在且已应用
|
||||
- [ ] 客户端已生成
|
||||
|
||||
### Hook/工具清单
|
||||
- [ ] 文件存在于预期路径
|
||||
- [ ] 导出函数
|
||||
- [ ] 有有意义的实现(非空返回)
|
||||
- [ ] 在应用某处被使用
|
||||
- [ ] 返回值被消费
|
||||
|
||||
### 连接清单
|
||||
- [ ] 组件 → API: fetch/axios 调用存在且使用响应
|
||||
- [ ] API → 数据库: 查询存在且结果返回
|
||||
- [ ] 表单 → 处理器: onSubmit 调用 API/mutation
|
||||
- [ ] 状态 → 渲染: 状态变量出现在 JSX 中
|
||||
|
||||
</verification_checklist>
|
||||
|
||||
<automated_verification_script>
|
||||
|
||||
## 自动化验证方法
|
||||
|
||||
对于验证子代理,使用此模式:
|
||||
|
||||
```bash
|
||||
# 1. 检查存在
|
||||
check_exists() {
|
||||
[ -f "$1" ] && echo "EXISTS: $1" || echo "MISSING: $1"
|
||||
}
|
||||
|
||||
# 2. 检查存根模式
|
||||
check_stubs() {
|
||||
local file="$1"
|
||||
local stubs=$(grep -c -E "TODO|FIXME|placeholder|not implemented" "$file" 2>/dev/null || echo 0)
|
||||
[ "$stubs" -gt 0 ] && echo "STUB_PATTERNS: $stubs in $file"
|
||||
}
|
||||
|
||||
# 3. 检查连接(组件调用 API)
|
||||
check_wiring() {
|
||||
local component="$1"
|
||||
local api_path="$2"
|
||||
grep -q "$api_path" "$component" && echo "WIRED: $component → $api_path" || echo "NOT_WIRED: $component → $api_path"
|
||||
}
|
||||
|
||||
# 4. 检查实质性(超过 N 行,有预期模式)
|
||||
check_substantive() {
|
||||
local file="$1"
|
||||
local min_lines="$2"
|
||||
local pattern="$3"
|
||||
local lines=$(wc -l < "$file" 2>/dev/null || echo 0)
|
||||
local has_pattern=$(grep -c -E "$pattern" "$file" 2>/dev/null || echo 0)
|
||||
[ "$lines" -ge "$min_lines" ] && [ "$has_pattern" -gt 0 ] && echo "SUBSTANTIVE: $file" || echo "THIN: $file ($lines lines, $has_pattern matches)"
|
||||
}
|
||||
```
|
||||
|
||||
对每个必须有工件运行这些检查。汇总结果到 VERIFICATION.md。
|
||||
|
||||
</automated_verification_script>
|
||||
|
||||
<human_verification_triggers>
|
||||
|
||||
## 何时需要人工验证
|
||||
|
||||
有些事情无法编程验证。标记这些需要人工测试:
|
||||
|
||||
**始终人工:**
|
||||
- 视觉外观(看起来对吗?)
|
||||
- 用户流程完成(能实际做那件事吗?)
|
||||
- 实时行为(WebSocket、SSE)
|
||||
- 外部服务集成(Stripe、邮件发送)
|
||||
- 错误消息清晰度(消息有帮助吗?)
|
||||
- 性能感觉(感觉快吗?)
|
||||
|
||||
**如不确定则人工:**
|
||||
- grep 无法追踪的复杂连接
|
||||
- 依赖状态的动态行为
|
||||
- 边缘情况和错误状态
|
||||
- 移动端响应式
|
||||
- 无障碍性
|
||||
|
||||
**人工验证请求格式:**
|
||||
```markdown
|
||||
## 需要人工验证
|
||||
|
||||
### 1. 聊天消息发送
|
||||
**测试:** 输入消息并点击发送
|
||||
**预期:** 消息出现在列表中,输入框清空
|
||||
**检查:** 刷新后消息是否持久?
|
||||
|
||||
### 2. 错误处理
|
||||
**测试:** 断开网络,尝试发送
|
||||
**预期:** 错误消息出现,消息未丢失
|
||||
**检查:** 重连后能重试吗?
|
||||
```
|
||||
|
||||
</human_verification_triggers>
|
||||
|
||||
<checkpoint_automation_reference>
|
||||
|
||||
## 检查点前自动化
|
||||
|
||||
关于自动化优先的检查点模式、服务器生命周期管理、CLI 安装处理和错误恢复协议,请参阅:
|
||||
|
||||
**@~/.claude/get-shit-done/references/checkpoints.md** → `<automation_reference>` 部分
|
||||
|
||||
关键原则:
|
||||
- Claude 在呈现检查点**之前**设置验证环境
|
||||
- 用户从不运行 CLI 命令(仅访问 URL)
|
||||
- 服务器生命周期:检查点前启动、处理端口冲突、持续运行
|
||||
- CLI 安装:安全处自动安装,否则检查点让用户选择
|
||||
- 错误处理:检查点前修复损坏环境,绝不呈现有失败设置的检查点
|
||||
|
||||
</checkpoint_automation_reference>
|
||||
@@ -15,9 +15,12 @@
|
||||
* state get [section] Get STATE.md content or section
|
||||
* state patch --field val ... Batch update STATE.md fields
|
||||
* state begin-phase --phase N --name S --plans C Update STATE.md for new phase start
|
||||
* state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal
|
||||
* state signal-resume Remove WAITING.json signal
|
||||
* resolve-model <agent-type> Get model for agent based on profile
|
||||
* find-phase <phase> Find phase directory by number
|
||||
* commit <message> [--files f1 f2] Commit planning docs
|
||||
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
||||
* commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
|
||||
* verify-summary <path> Verify a SUMMARY.md file
|
||||
* generate-slug <text> Convert text to URL-safe slug
|
||||
* current-timestamp [format] Get timestamp (full|date|filename)
|
||||
@@ -33,7 +36,7 @@
|
||||
*
|
||||
* Phase Operations:
|
||||
* phase next-decimal <phase> Calculate next decimal phase number
|
||||
* phase add <description> Append new phase to roadmap + create dir
|
||||
* phase add <description> [--id ID] Append new phase to roadmap + create dir
|
||||
* phase insert <after> <description> Insert decimal phase after existing
|
||||
* phase remove <phase> [--force] Remove phase, renumber all subsequent
|
||||
* phase complete <phase> Mark phase done, update state + roadmap
|
||||
@@ -62,6 +65,9 @@
|
||||
* Todos:
|
||||
* todo complete <filename> Move todo from pending to completed
|
||||
*
|
||||
* UAT Audit:
|
||||
* audit-uat Scan all phases for unresolved UAT/verification items
|
||||
*
|
||||
* Scaffolding:
|
||||
* scaffold context --phase <N> Create CONTEXT.md template
|
||||
* scaffold uat --phase <N> Create UAT.md template
|
||||
@@ -129,7 +135,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { error } = require('./lib/core.cjs');
|
||||
const { error, findProjectRoot } = require('./lib/core.cjs');
|
||||
const state = require('./lib/state.cjs');
|
||||
const phase = require('./lib/phase.cjs');
|
||||
const roadmap = require('./lib/roadmap.cjs');
|
||||
@@ -168,6 +174,13 @@ async function main() {
|
||||
error(`Invalid --cwd: ${cwd}`);
|
||||
}
|
||||
|
||||
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree
|
||||
const { resolveWorktreeRoot } = require('./lib/core.cjs');
|
||||
const worktreeRoot = resolveWorktreeRoot(cwd);
|
||||
if (worktreeRoot !== cwd) {
|
||||
cwd = worktreeRoot;
|
||||
}
|
||||
|
||||
const rawIndex = args.indexOf('--raw');
|
||||
const raw = rawIndex !== -1;
|
||||
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
||||
@@ -178,6 +191,17 @@ async function main() {
|
||||
error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init');
|
||||
}
|
||||
|
||||
// Multi-repo guard: resolve project root for commands that read/write .planning/.
|
||||
// Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary
|
||||
// filesystem traversal on every invocation.
|
||||
const SKIP_ROOT_RESOLUTION = new Set([
|
||||
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
||||
'verify-summary', 'template', 'frontmatter',
|
||||
]);
|
||||
if (!SKIP_ROOT_RESOLUTION.has(command)) {
|
||||
cwd = findProjectRoot(cwd);
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'state': {
|
||||
const subcommand = args[1];
|
||||
@@ -255,6 +279,21 @@ async function main() {
|
||||
plansIdx !== -1 ? parseInt(args[plansIdx + 1], 10) : null,
|
||||
raw
|
||||
);
|
||||
} else if (subcommand === 'signal-waiting') {
|
||||
const typeIdx = args.indexOf('--type');
|
||||
const qIdx = args.indexOf('--question');
|
||||
const optIdx = args.indexOf('--options');
|
||||
const phaseIdx = args.indexOf('--phase');
|
||||
state.cmdSignalWaiting(
|
||||
cwd,
|
||||
typeIdx !== -1 ? args[typeIdx + 1] : null,
|
||||
qIdx !== -1 ? args[qIdx + 1] : null,
|
||||
optIdx !== -1 ? args[optIdx + 1] : null,
|
||||
phaseIdx !== -1 ? args[phaseIdx + 1] : null,
|
||||
raw
|
||||
);
|
||||
} else if (subcommand === 'signal-resume') {
|
||||
state.cmdSignalResume(cwd, raw);
|
||||
} else {
|
||||
state.cmdStateLoad(cwd, raw);
|
||||
}
|
||||
@@ -273,6 +312,7 @@ async function main() {
|
||||
|
||||
case 'commit': {
|
||||
const amend = args.includes('--amend');
|
||||
const noVerify = args.includes('--no-verify');
|
||||
const filesIndex = args.indexOf('--files');
|
||||
// Collect all positional args between command name and first flag,
|
||||
// then join them — handles both quoted ("multi word msg") and
|
||||
@@ -281,7 +321,15 @@ async function main() {
|
||||
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
||||
const message = messageArgs.join(' ') || undefined;
|
||||
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
||||
commands.cmdCommit(cwd, message, files, raw, amend);
|
||||
commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'commit-to-subrepo': {
|
||||
const message = args[1];
|
||||
const filesIndex = args.indexOf('--files');
|
||||
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
||||
commands.cmdCommitToSubrepo(cwd, message, files, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -457,7 +505,18 @@ async function main() {
|
||||
if (subcommand === 'next-decimal') {
|
||||
phase.cmdPhaseNextDecimal(cwd, args[2], raw);
|
||||
} else if (subcommand === 'add') {
|
||||
phase.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
|
||||
const idIdx = args.indexOf('--id');
|
||||
let customId = null;
|
||||
const descArgs = [];
|
||||
for (let i = 2; i < args.length; i++) {
|
||||
if (args[i] === '--id' && i + 1 < args.length) {
|
||||
customId = args[i + 1];
|
||||
i++; // skip value
|
||||
} else {
|
||||
descArgs.push(args[i]);
|
||||
}
|
||||
}
|
||||
phase.cmdPhaseAdd(cwd, descArgs.join(' '), raw, customId);
|
||||
} else if (subcommand === 'insert') {
|
||||
phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
|
||||
} else if (subcommand === 'remove') {
|
||||
@@ -512,6 +571,12 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'audit-uat': {
|
||||
const uat = require('./lib/uat.cjs');
|
||||
uat.cmdAuditUat(cwd, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'stats': {
|
||||
const subcommand = args[1] || 'json';
|
||||
commands.cmdStats(cwd, subcommand, raw);
|
||||
@@ -522,8 +587,10 @@ async function main() {
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'complete') {
|
||||
commands.cmdTodoComplete(cwd, args[2], raw);
|
||||
} else if (subcommand === 'match-phase') {
|
||||
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
||||
} else {
|
||||
error('Unknown todo subcommand. Available: complete');
|
||||
error('Unknown todo subcommand. Available: complete, match-phase');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
const { execSync, execFileSync, spawnSync } = require('child_process');
|
||||
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
||||
|
||||
// ─── Path helpers ────────────────────────────────────────────────────────────
|
||||
@@ -14,6 +14,102 @@ function toPosixPath(p) {
|
||||
return p.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan immediate child directories for separate git repos.
|
||||
* Returns a sorted array of directory names that have their own `.git`.
|
||||
* Excludes hidden directories and node_modules.
|
||||
*/
|
||||
function detectSubRepos(cwd) {
|
||||
const results = [];
|
||||
try {
|
||||
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
||||
const gitPath = path.join(cwd, entry.name, '.git');
|
||||
try {
|
||||
if (fs.existsSync(gitPath)) {
|
||||
results.push(entry.name);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
return results.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk up from `startDir` to find the project root that owns `.planning/`.
|
||||
*
|
||||
* In multi-repo workspaces, Claude may open inside a sub-repo (e.g. `backend/`)
|
||||
* instead of the project root. This function prevents `.planning/` from being
|
||||
* created inside the sub-repo by locating the nearest ancestor that already has
|
||||
* a `.planning/` directory.
|
||||
*
|
||||
* Detection strategy (checked in order for each ancestor):
|
||||
* 1. Parent has `.planning/config.json` with `sub_repos` listing this directory
|
||||
* 2. Parent has `.planning/config.json` with `multiRepo: true` (legacy format)
|
||||
* 3. Parent has `.planning/` and current dir has its own `.git` (heuristic)
|
||||
*
|
||||
* Returns `startDir` unchanged when no ancestor `.planning/` is found (first-run
|
||||
* or single-repo projects).
|
||||
*/
|
||||
function findProjectRoot(startDir) {
|
||||
const resolved = path.resolve(startDir);
|
||||
const root = path.parse(resolved).root;
|
||||
const homedir = require('os').homedir();
|
||||
|
||||
// Check if startDir or any of its ancestors (up to but not including a
|
||||
// candidate project root) contains a .git directory. This handles both
|
||||
// `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside).
|
||||
function isInsideGitRepo(candidateParent) {
|
||||
let d = resolved;
|
||||
while (d !== candidateParent && d !== root) {
|
||||
if (fs.existsSync(path.join(d, '.git'))) return true;
|
||||
d = path.dirname(d);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let dir = resolved;
|
||||
while (dir !== root) {
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break; // filesystem root
|
||||
if (parent === homedir) break; // never go above home
|
||||
|
||||
const parentPlanning = path.join(parent, '.planning');
|
||||
if (fs.existsSync(parentPlanning) && fs.statSync(parentPlanning).isDirectory()) {
|
||||
const configPath = path.join(parentPlanning, 'config.json');
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
const subRepos = config.sub_repos || config.planning?.sub_repos || [];
|
||||
|
||||
// Check explicit sub_repos list
|
||||
if (Array.isArray(subRepos) && subRepos.length > 0) {
|
||||
const relPath = path.relative(parent, resolved);
|
||||
const topSegment = relPath.split(path.sep)[0];
|
||||
if (subRepos.includes(topSegment)) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Check legacy multiRepo flag
|
||||
if (config.multiRepo === true && isInsideGitRepo(parent)) {
|
||||
return parent;
|
||||
}
|
||||
} catch {
|
||||
// config.json missing or malformed — fall back to .git heuristic
|
||||
}
|
||||
|
||||
// Heuristic: parent has .planning/ and we're inside a git repo
|
||||
if (isInsideGitRepo(parent)) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
return startDir;
|
||||
}
|
||||
|
||||
// ─── Output helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function output(result, raw, rawValue) {
|
||||
@@ -58,12 +154,18 @@ function loadConfig(cwd) {
|
||||
branching_strategy: 'none',
|
||||
phase_branch_template: 'gsd/phase-{phase}-{slug}',
|
||||
milestone_branch_template: 'gsd/{milestone}-{slug}',
|
||||
quick_branch_template: null,
|
||||
research: true,
|
||||
plan_checker: true,
|
||||
verifier: true,
|
||||
nyquist_validation: true,
|
||||
parallelization: true,
|
||||
brave_search: false,
|
||||
text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus
|
||||
sub_repos: [],
|
||||
resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs
|
||||
context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models
|
||||
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -75,6 +177,39 @@ function loadConfig(cwd) {
|
||||
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
||||
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
||||
delete parsed.depth;
|
||||
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
// Auto-detect and sync sub_repos: scan for child directories with .git
|
||||
let configDirty = false;
|
||||
|
||||
// Migrate legacy "multiRepo: true" boolean → sub_repos array
|
||||
if (parsed.multiRepo === true && !parsed.sub_repos && !parsed.planning?.sub_repos) {
|
||||
const detected = detectSubRepos(cwd);
|
||||
if (detected.length > 0) {
|
||||
parsed.sub_repos = detected;
|
||||
if (!parsed.planning) parsed.planning = {};
|
||||
parsed.planning.commit_docs = false;
|
||||
delete parsed.multiRepo;
|
||||
configDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep sub_repos in sync with actual filesystem
|
||||
const currentSubRepos = parsed.sub_repos || parsed.planning?.sub_repos || [];
|
||||
if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) {
|
||||
const detected = detectSubRepos(cwd);
|
||||
if (detected.length > 0) {
|
||||
const sorted = [...currentSubRepos].sort();
|
||||
if (JSON.stringify(sorted) !== JSON.stringify(detected)) {
|
||||
parsed.sub_repos = detected;
|
||||
configDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persist sub_repos changes (migration or sync)
|
||||
if (configDirty) {
|
||||
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
|
||||
}
|
||||
|
||||
@@ -100,12 +235,18 @@ function loadConfig(cwd) {
|
||||
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
||||
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
||||
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
||||
quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template,
|
||||
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
||||
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
||||
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
||||
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
||||
parallelization,
|
||||
brave_search: get('brave_search') ?? defaults.brave_search,
|
||||
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
|
||||
sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
|
||||
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
|
||||
context_window: get('context_window') ?? defaults.context_window,
|
||||
phase_naming: get('phase_naming') ?? defaults.phase_naming,
|
||||
model_overrides: parsed.model_overrides || null,
|
||||
};
|
||||
} catch {
|
||||
@@ -121,7 +262,9 @@ function isGitIgnored(cwd, targetPath) {
|
||||
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
||||
// .gitignore explicitly lists them — a common source of confusion when .planning/
|
||||
// was committed before being added to .gitignore.
|
||||
execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
||||
// Use execFileSync (array args) to prevent shell interpretation of special characters
|
||||
// in file paths — avoids command injection via crafted path names.
|
||||
execFileSync('git', ['check-ignore', '-q', '--no-index', '--', targetPath], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
@@ -248,6 +391,105 @@ function execGit(cwd, args) {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Common path helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve the main worktree root when running inside a git worktree.
|
||||
* In a linked worktree, .planning/ lives in the main worktree, not in the linked one.
|
||||
* Returns the main worktree path, or cwd if not in a worktree.
|
||||
*/
|
||||
function resolveWorktreeRoot(cwd) {
|
||||
// Check if we're in a linked worktree
|
||||
const gitDir = execGit(cwd, ['rev-parse', '--git-dir']);
|
||||
const commonDir = execGit(cwd, ['rev-parse', '--git-common-dir']);
|
||||
|
||||
if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return cwd;
|
||||
|
||||
// In a linked worktree, .git is a file pointing to .git/worktrees/<name>
|
||||
// and git-common-dir points to the main repo's .git directory
|
||||
const gitDirResolved = path.resolve(cwd, gitDir.stdout);
|
||||
const commonDirResolved = path.resolve(cwd, commonDir.stdout);
|
||||
|
||||
if (gitDirResolved !== commonDirResolved) {
|
||||
// We're in a linked worktree — resolve main worktree root
|
||||
// The common dir is the main repo's .git, so its parent is the main worktree root
|
||||
return path.dirname(commonDirResolved);
|
||||
}
|
||||
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a file-based lock for .planning/ writes.
|
||||
* Prevents concurrent worktrees from corrupting shared planning files.
|
||||
* Lock is auto-released after the callback completes.
|
||||
*/
|
||||
function withPlanningLock(cwd, fn) {
|
||||
const lockPath = path.join(planningDir(cwd), '.lock');
|
||||
const lockTimeout = 10000; // 10 seconds
|
||||
const retryDelay = 100;
|
||||
const start = Date.now();
|
||||
|
||||
// Ensure .planning/ exists
|
||||
try { fs.mkdirSync(planningDir(cwd), { recursive: true }); } catch { /* ok */ }
|
||||
|
||||
while (Date.now() - start < lockTimeout) {
|
||||
try {
|
||||
// Atomic create — fails if file exists
|
||||
fs.writeFileSync(lockPath, JSON.stringify({
|
||||
pid: process.pid,
|
||||
cwd,
|
||||
acquired: new Date().toISOString(),
|
||||
}), { flag: 'wx' });
|
||||
|
||||
// Lock acquired — run the function
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
try { fs.unlinkSync(lockPath); } catch { /* already released */ }
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'EEXIST') {
|
||||
// Lock exists — check if stale (>30s old)
|
||||
try {
|
||||
const stat = fs.statSync(lockPath);
|
||||
if (Date.now() - stat.mtimeMs > 30000) {
|
||||
fs.unlinkSync(lockPath);
|
||||
continue; // retry
|
||||
}
|
||||
} catch { continue; }
|
||||
|
||||
// Wait and retry
|
||||
spawnSync('sleep', ['0.1'], { stdio: 'ignore' });
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// Timeout — force acquire (stale lock recovery)
|
||||
try { fs.unlinkSync(lockPath); } catch { /* ok */ }
|
||||
return fn();
|
||||
}
|
||||
|
||||
/** Get the .planning directory path */
|
||||
function planningDir(cwd) {
|
||||
return path.join(cwd, '.planning');
|
||||
}
|
||||
|
||||
/** Get common .planning file paths */
|
||||
function planningPaths(cwd) {
|
||||
const base = path.join(cwd, '.planning');
|
||||
return {
|
||||
planning: base,
|
||||
state: path.join(base, 'STATE.md'),
|
||||
roadmap: path.join(base, 'ROADMAP.md'),
|
||||
project: path.join(base, 'PROJECT.md'),
|
||||
config: path.join(base, 'config.json'),
|
||||
phases: path.join(base, 'phases'),
|
||||
requirements: path.join(base, 'REQUIREMENTS.md'),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
||||
|
||||
function escapeRegex(value) {
|
||||
@@ -255,17 +497,23 @@ function escapeRegex(value) {
|
||||
}
|
||||
|
||||
function normalizePhaseName(phase) {
|
||||
const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
||||
if (!match) return phase;
|
||||
const padded = match[1].padStart(2, '0');
|
||||
const letter = match[2] ? match[2].toUpperCase() : '';
|
||||
const decimal = match[3] || '';
|
||||
return padded + letter + decimal;
|
||||
const str = String(phase);
|
||||
// Standard numeric phases: 1, 01, 12A, 12.1
|
||||
const match = str.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
||||
if (match) {
|
||||
const padded = match[1].padStart(2, '0');
|
||||
const letter = match[2] ? match[2].toUpperCase() : '';
|
||||
const decimal = match[3] || '';
|
||||
return padded + letter + decimal;
|
||||
}
|
||||
// Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is
|
||||
return str;
|
||||
}
|
||||
|
||||
function comparePhaseNum(a, b) {
|
||||
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
||||
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
||||
// If either is non-numeric (custom ID), fall back to string comparison
|
||||
if (!pa || !pb) return String(a).localeCompare(String(b));
|
||||
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
||||
if (intDiff !== 0) return intDiff;
|
||||
@@ -295,10 +543,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
||||
try {
|
||||
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
||||
const match = dirs.find(d => d.startsWith(normalized));
|
||||
// Match: starts with normalized (numeric) OR contains normalized as prefix segment (custom ID)
|
||||
const match = dirs.find(d => {
|
||||
if (d.startsWith(normalized)) return true;
|
||||
// For custom IDs like PROJ-42, match case-insensitively
|
||||
if (d.toUpperCase().startsWith(normalized.toUpperCase())) return true;
|
||||
return false;
|
||||
});
|
||||
if (!match) return null;
|
||||
|
||||
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
||||
// Extract phase number and name — supports both numeric (01-name) and custom (PROJ-42-name)
|
||||
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|
||||
|| match.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-(.+)/i)
|
||||
|| [null, match, null];
|
||||
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
||||
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
||||
const phaseDir = path.join(baseDir, match);
|
||||
@@ -368,7 +625,7 @@ function findPhaseInternal(cwd, phase) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -403,7 +660,7 @@ function getArchivedPhaseDirs(cwd) {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -420,6 +677,91 @@ function stripShippedMilestones(content) {
|
||||
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
||||
*
|
||||
* Instead of stripping <details> blocks (negative heuristic that breaks if
|
||||
* agents wrap the current milestone in <details>), this finds the section
|
||||
* matching the current milestone version and returns only that content.
|
||||
*
|
||||
* Falls back to stripShippedMilestones() if:
|
||||
* - cwd is not provided
|
||||
* - STATE.md doesn't exist or has no milestone field
|
||||
* - Version can't be found in ROADMAP.md
|
||||
*
|
||||
* @param {string} content - Full ROADMAP.md content
|
||||
* @param {string} [cwd] - Working directory for reading STATE.md
|
||||
* @returns {string} Content scoped to current milestone
|
||||
*/
|
||||
function extractCurrentMilestone(content, cwd) {
|
||||
if (!cwd) return stripShippedMilestones(content);
|
||||
|
||||
// 1. Get current milestone version from STATE.md frontmatter
|
||||
let version = null;
|
||||
try {
|
||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||
if (fs.existsSync(statePath)) {
|
||||
const stateRaw = fs.readFileSync(statePath, 'utf-8');
|
||||
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
|
||||
if (milestoneMatch) {
|
||||
version = milestoneMatch[1].trim();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 2. Fallback: derive version from getMilestoneInfo pattern in ROADMAP.md itself
|
||||
if (!version) {
|
||||
// Check for 🚧 in-progress marker
|
||||
const inProgressMatch = content.match(/🚧\s*\*\*v(\d+\.\d+)\s/);
|
||||
if (inProgressMatch) {
|
||||
version = 'v' + inProgressMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!version) return stripShippedMilestones(content);
|
||||
|
||||
// 3. Find the section matching this version
|
||||
// Match headings like: ## Roadmap v3.0: Name, ## v3.0 Name, etc.
|
||||
const escapedVersion = escapeRegex(version);
|
||||
const sectionPattern = new RegExp(
|
||||
`(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`,
|
||||
'mi'
|
||||
);
|
||||
const sectionMatch = content.match(sectionPattern);
|
||||
|
||||
if (!sectionMatch) return stripShippedMilestones(content);
|
||||
|
||||
const sectionStart = sectionMatch.index;
|
||||
|
||||
// Find the end: next milestone heading at same or higher level, or EOF
|
||||
// Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc.
|
||||
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
|
||||
const restContent = content.slice(sectionStart + sectionMatch[0].length);
|
||||
const nextMilestonePattern = new RegExp(
|
||||
`^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`,
|
||||
'mi'
|
||||
);
|
||||
const nextMatch = restContent.match(nextMilestonePattern);
|
||||
|
||||
let sectionEnd;
|
||||
if (nextMatch) {
|
||||
sectionEnd = sectionStart + sectionMatch[0].length + nextMatch.index;
|
||||
} else {
|
||||
sectionEnd = content.length;
|
||||
}
|
||||
|
||||
// Return everything before the current milestone section (non-milestone content
|
||||
// like title, overview) plus the current milestone section
|
||||
const beforeMilestones = content.slice(0, sectionStart);
|
||||
const currentSection = content.slice(sectionStart, sectionEnd);
|
||||
|
||||
// Also include any content before the first milestone heading (title, overview, etc.)
|
||||
// but strip any <details> blocks in it (these are definitely shipped)
|
||||
const preamble = beforeMilestones.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
||||
|
||||
return preamble + currentSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a pattern only in the current milestone section of ROADMAP.md
|
||||
* (everything after the last </details> close tag). Used for write operations
|
||||
@@ -444,8 +786,9 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
if (!fs.existsSync(roadmapPath)) return null;
|
||||
|
||||
try {
|
||||
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
|
||||
const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
|
||||
const escapedPhase = escapeRegex(phaseNum.toString());
|
||||
// Match both numeric (Phase 1:) and custom (Phase PROJ-42:) headers
|
||||
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
||||
const headerMatch = content.match(phasePattern);
|
||||
if (!headerMatch) return null;
|
||||
@@ -453,7 +796,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
const phaseName = headerMatch[1].trim();
|
||||
const headerIndex = headerMatch.index;
|
||||
const restOfContent = content.slice(headerIndex);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+[\w]/i);
|
||||
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
||||
const section = content.slice(headerIndex, sectionEnd).trim();
|
||||
|
||||
@@ -472,6 +815,19 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Model alias resolution ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Map short model aliases to full model IDs.
|
||||
* Updated each release to match current model versions.
|
||||
* Users can override with model_overrides in config.json for custom/latest models.
|
||||
*/
|
||||
const MODEL_ALIAS_MAP = {
|
||||
'opus': 'claude-opus-4-0',
|
||||
'sonnet': 'claude-sonnet-4-5',
|
||||
'haiku': 'claude-haiku-3-5',
|
||||
};
|
||||
|
||||
function resolveModelInternal(cwd, agentType) {
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
@@ -486,7 +842,32 @@ function resolveModelInternal(cwd, agentType) {
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
if (!agentModels) return 'sonnet';
|
||||
if (profile === 'inherit') return 'inherit';
|
||||
return agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||
|
||||
// If resolve_model_ids is true, map alias to full model ID
|
||||
// This prevents 404s when the Task tool passes aliases directly to the API
|
||||
if (config.resolve_model_ids) {
|
||||
return MODEL_ALIAS_MAP[alias] || alias;
|
||||
}
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
// ─── Summary body helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract a one-liner from the summary body when it's not in frontmatter.
|
||||
* The summary template defines one-liner as a bold markdown line after the heading:
|
||||
* # Phase X: Name Summary
|
||||
* **[substantive one-liner text]**
|
||||
*/
|
||||
function extractOneLinerFromBody(content) {
|
||||
if (!content) return null;
|
||||
// Strip frontmatter first
|
||||
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
||||
// Find the first **...** line after a # heading
|
||||
const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
||||
@@ -512,7 +893,8 @@ function getMilestoneInfo(cwd) {
|
||||
|
||||
// First: check for list-format roadmaps using 🚧 (in-progress) marker
|
||||
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
||||
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
||||
// e.g. "- 🚧 **v1.2.1 Tech Debt** — Phases 1-8 (in progress)"
|
||||
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
|
||||
if (inProgressMatch) {
|
||||
return {
|
||||
version: 'v' + inProgressMatch[1],
|
||||
@@ -523,15 +905,16 @@ function getMilestoneInfo(cwd) {
|
||||
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
||||
const cleaned = stripShippedMilestones(roadmap);
|
||||
// Extract version and name from the same ## heading for consistency
|
||||
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
||||
// Supports 2+ segment versions: v1.2, v1.2.1, v2.0.1, etc.
|
||||
const headingMatch = cleaned.match(/## .*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/);
|
||||
if (headingMatch) {
|
||||
return {
|
||||
version: 'v' + headingMatch[1],
|
||||
name: headingMatch[2].trim(),
|
||||
};
|
||||
}
|
||||
// Fallback: try bare version match
|
||||
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
||||
// Fallback: try bare version match (greedy — capture longest version string)
|
||||
const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
|
||||
return {
|
||||
version: versionMatch ? versionMatch[0] : 'v1.0',
|
||||
name: 'milestone',
|
||||
@@ -549,13 +932,14 @@ function getMilestoneInfo(cwd) {
|
||||
function getMilestonePhaseFilter(cwd) {
|
||||
const milestonePhaseNums = new Set();
|
||||
try {
|
||||
const roadmap = stripShippedMilestones(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'));
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
||||
const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd);
|
||||
// Match both numeric phases (Phase 1:) and custom IDs (Phase PROJ-42:)
|
||||
const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(roadmap)) !== null) {
|
||||
milestonePhaseNums.add(m[1]);
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
if (milestonePhaseNums.size === 0) {
|
||||
const passAll = () => true;
|
||||
@@ -568,9 +952,13 @@ function getMilestonePhaseFilter(cwd) {
|
||||
);
|
||||
|
||||
function isDirInMilestone(dirName) {
|
||||
// Try numeric match first
|
||||
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
||||
if (!m) return false;
|
||||
return normalized.has(m[1].toLowerCase());
|
||||
if (m && normalized.has(m[1].toLowerCase())) return true;
|
||||
// Try custom ID match (e.g. PROJ-42-description → PROJ-42)
|
||||
const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
|
||||
if (customMatch && normalized.has(customMatch[1].toLowerCase())) return true;
|
||||
return false;
|
||||
}
|
||||
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
||||
return isDirInMilestone;
|
||||
@@ -597,6 +985,15 @@ module.exports = {
|
||||
getMilestoneInfo,
|
||||
getMilestonePhaseFilter,
|
||||
stripShippedMilestones,
|
||||
extractCurrentMilestone,
|
||||
replaceInCurrentMilestone,
|
||||
toPosixPath,
|
||||
extractOneLinerFromBody,
|
||||
resolveWorktreeRoot,
|
||||
withPlanningLock,
|
||||
findProjectRoot,
|
||||
detectSubRepos,
|
||||
MODEL_ALIAS_MAP,
|
||||
planningDir,
|
||||
planningPaths,
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
|
||||
const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { writeStateMd } = require('./state.cjs');
|
||||
const { writeStateMd, stateReplaceFieldWithFallback } = require('./state.cjs');
|
||||
|
||||
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
||||
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
||||
@@ -25,7 +25,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
||||
error('no valid requirement IDs found');
|
||||
}
|
||||
|
||||
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
|
||||
const reqPath = planningPaths(cwd).requirements;
|
||||
if (!fs.existsSync(reqPath)) {
|
||||
output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
|
||||
return;
|
||||
@@ -90,12 +90,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
error('version required for milestone complete (e.g., v1.0)');
|
||||
}
|
||||
|
||||
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
|
||||
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
|
||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||
const roadmapPath = planningPaths(cwd).roadmap;
|
||||
const reqPath = planningPaths(cwd).requirements;
|
||||
const statePath = planningPaths(cwd).state;
|
||||
const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
|
||||
const archiveDir = path.join(cwd, '.planning', 'milestones');
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
const phasesDir = planningPaths(cwd).phases;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const milestoneName = options.name || version;
|
||||
|
||||
@@ -131,16 +131,24 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
|
||||
const fm = extractFrontmatter(content);
|
||||
if (fm['one-liner']) {
|
||||
accomplishments.push(fm['one-liner']);
|
||||
const oneLiner = fm['one-liner'] || extractOneLinerFromBody(content);
|
||||
if (oneLiner) {
|
||||
accomplishments.push(oneLiner);
|
||||
}
|
||||
// Count tasks
|
||||
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
||||
totalTasks += taskMatches.length;
|
||||
} catch {}
|
||||
// Count tasks: prefer **Tasks:** N from Performance section,
|
||||
// then <task XML tags, then ## Task N markdown headers
|
||||
const tasksFieldMatch = content.match(/\*\*Tasks:\*\*\s*(\d+)/);
|
||||
if (tasksFieldMatch) {
|
||||
totalTasks += parseInt(tasksFieldMatch[1], 10);
|
||||
} else {
|
||||
const xmlTaskMatches = content.match(/<task[\s>]/gi) || [];
|
||||
const mdTaskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
||||
totalTasks += xmlTaskMatches.length || mdTaskMatches.length;
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Archive ROADMAP.md
|
||||
if (fs.existsSync(roadmapPath)) {
|
||||
@@ -186,21 +194,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
|
||||
}
|
||||
|
||||
// Update STATE.md
|
||||
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
||||
if (fs.existsSync(statePath)) {
|
||||
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
stateContent = stateContent.replace(
|
||||
/(\*\*Status:\*\*\s*).*/,
|
||||
`$1${version} milestone complete`
|
||||
);
|
||||
stateContent = stateContent.replace(
|
||||
/(\*\*Last Activity:\*\*\s*).*/,
|
||||
`$1${today}`
|
||||
);
|
||||
stateContent = stateContent.replace(
|
||||
/(\*\*Last Activity Description:\*\*\s*).*/,
|
||||
`$1${version} milestone completed and archived`
|
||||
);
|
||||
|
||||
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
|
||||
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
||||
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
||||
`${version} milestone completed and archived`);
|
||||
|
||||
writeStateMd(statePath, stateContent, cwd);
|
||||
}
|
||||
|
||||
@@ -220,7 +222,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
archivedCount++;
|
||||
}
|
||||
phasesArchived = archivedCount > 0;
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
const result = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -179,6 +179,17 @@ const CLAUDE_MD_FALLBACKS = {
|
||||
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
|
||||
};
|
||||
|
||||
const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
|
||||
'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
|
||||
'',
|
||||
'Use these entry points:',
|
||||
'- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks',
|
||||
'- `/gsd:debug` for investigation and bug fixing',
|
||||
'- `/gsd:execute-phase` for planned phase work',
|
||||
'',
|
||||
'Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.',
|
||||
].join('\n');
|
||||
|
||||
const CLAUDE_MD_PROFILE_PLACEHOLDER = [
|
||||
'<!-- GSD:profile-start -->',
|
||||
'## Developer Profile',
|
||||
@@ -356,6 +367,14 @@ function generateArchitectureSection(cwd) {
|
||||
return { content: summary, source: 'ARCHITECTURE.md', hasFallback: false };
|
||||
}
|
||||
|
||||
function generateWorkflowSection() {
|
||||
return {
|
||||
content: CLAUDE_MD_WORKFLOW_ENFORCEMENT,
|
||||
source: 'GSD defaults',
|
||||
hasFallback: false,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Commands ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmdWriteProfile(cwd, options, raw) {
|
||||
@@ -796,18 +815,20 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
||||
}
|
||||
|
||||
function cmdGenerateClaudeMd(cwd, options, raw) {
|
||||
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture'];
|
||||
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'workflow'];
|
||||
const generators = {
|
||||
project: generateProjectSection,
|
||||
stack: generateStackSection,
|
||||
conventions: generateConventionsSection,
|
||||
architecture: generateArchitectureSection,
|
||||
workflow: generateWorkflowSection,
|
||||
};
|
||||
const sectionHeadings = {
|
||||
project: '## Project',
|
||||
stack: '## Technology Stack',
|
||||
conventions: '## Conventions',
|
||||
architecture: '## Architecture',
|
||||
workflow: '## GSD Workflow Enforcement',
|
||||
};
|
||||
|
||||
const generated = {};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
189
get-shit-done/bin/lib/uat.cjs
Normal file
189
get-shit-done/bin/lib/uat.cjs
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* UAT Audit — Cross-phase UAT/VERIFICATION scanner
|
||||
*
|
||||
* Reads all *-UAT.md and *-VERIFICATION.md files across all phases.
|
||||
* Extracts non-passing items. Returns structured JSON for workflow consumption.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { output, error, getMilestonePhaseFilter } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
|
||||
function cmdAuditUat(cwd, raw) {
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
if (!fs.existsSync(phasesDir)) {
|
||||
error('No .planning/phases directory found');
|
||||
}
|
||||
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const results = [];
|
||||
|
||||
// Scan all phase directories
|
||||
const dirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name)
|
||||
.filter(isDirInMilestone)
|
||||
.sort();
|
||||
|
||||
for (const dir of dirs) {
|
||||
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
const phaseNum = phaseMatch ? phaseMatch[1] : dir;
|
||||
const phaseDir = path.join(phasesDir, dir);
|
||||
const files = fs.readdirSync(phaseDir);
|
||||
|
||||
// Process UAT files
|
||||
for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
||||
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
|
||||
const items = parseUatItems(content);
|
||||
if (items.length > 0) {
|
||||
results.push({
|
||||
phase: phaseNum,
|
||||
phase_dir: dir,
|
||||
file,
|
||||
file_path: `.planning/phases/${dir}/${file}`,
|
||||
type: 'uat',
|
||||
status: (extractFrontmatter(content).status || 'unknown'),
|
||||
items,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process VERIFICATION files
|
||||
for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
||||
const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8');
|
||||
const status = extractFrontmatter(content).status || 'unknown';
|
||||
if (status === 'human_needed' || status === 'gaps_found') {
|
||||
const items = parseVerificationItems(content, status);
|
||||
if (items.length > 0) {
|
||||
results.push({
|
||||
phase: phaseNum,
|
||||
phase_dir: dir,
|
||||
file,
|
||||
file_path: `.planning/phases/${dir}/${file}`,
|
||||
type: 'verification',
|
||||
status,
|
||||
items,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute summary
|
||||
const summary = {
|
||||
total_files: results.length,
|
||||
total_items: results.reduce((sum, r) => sum + r.items.length, 0),
|
||||
by_category: {},
|
||||
by_phase: {},
|
||||
};
|
||||
|
||||
for (const r of results) {
|
||||
if (!summary.by_phase[r.phase]) summary.by_phase[r.phase] = 0;
|
||||
for (const item of r.items) {
|
||||
summary.by_phase[r.phase]++;
|
||||
const cat = item.category || 'unknown';
|
||||
summary.by_category[cat] = (summary.by_category[cat] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
output({ results, summary }, raw);
|
||||
}
|
||||
|
||||
function parseUatItems(content) {
|
||||
const items = [];
|
||||
// Match test blocks: ### N. Name\nexpected: ...\nresult: ...\n
|
||||
const testPattern = /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)(?:\n(?:reported|reason|blocked_by):\s*[^\n]*)?/g;
|
||||
let match;
|
||||
while ((match = testPattern.exec(content)) !== null) {
|
||||
const [, num, name, expected, result] = match;
|
||||
if (result === 'pending' || result === 'skipped' || result === 'blocked') {
|
||||
// Extract optional fields — limit to current test block (up to next ### or EOF)
|
||||
const afterMatch = content.slice(match.index);
|
||||
const nextHeading = afterMatch.indexOf('\n###', 1);
|
||||
const blockText = nextHeading > 0 ? afterMatch.slice(0, nextHeading) : afterMatch;
|
||||
const reasonMatch = blockText.match(/reason:\s*(.+)/);
|
||||
const blockedByMatch = blockText.match(/blocked_by:\s*(.+)/);
|
||||
|
||||
const item = {
|
||||
test: parseInt(num, 10),
|
||||
name: name.trim(),
|
||||
expected: expected.trim(),
|
||||
result,
|
||||
category: categorizeItem(result, reasonMatch?.[1], blockedByMatch?.[1]),
|
||||
};
|
||||
if (reasonMatch) item.reason = reasonMatch[1].trim();
|
||||
if (blockedByMatch) item.blocked_by = blockedByMatch[1].trim();
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function parseVerificationItems(content, status) {
|
||||
const items = [];
|
||||
if (status === 'human_needed') {
|
||||
// Extract from human_verification section — look for numbered items or table rows
|
||||
const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i);
|
||||
if (hvSection) {
|
||||
const lines = hvSection[1].split('\n');
|
||||
for (const line of lines) {
|
||||
// Match table rows: | N | description | ... |
|
||||
const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/);
|
||||
// Match bullet items: - description
|
||||
const bulletMatch = line.match(/^[-*]\s+(.+)/);
|
||||
// Match numbered items: 1. description
|
||||
const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
|
||||
|
||||
if (tableMatch) {
|
||||
items.push({
|
||||
test: parseInt(tableMatch[1], 10),
|
||||
name: tableMatch[2].trim(),
|
||||
result: 'human_needed',
|
||||
category: 'human_uat',
|
||||
});
|
||||
} else if (numberedMatch) {
|
||||
items.push({
|
||||
test: parseInt(numberedMatch[1], 10),
|
||||
name: numberedMatch[2].trim(),
|
||||
result: 'human_needed',
|
||||
category: 'human_uat',
|
||||
});
|
||||
} else if (bulletMatch && bulletMatch[1].length > 10) {
|
||||
items.push({
|
||||
name: bulletMatch[1].trim(),
|
||||
result: 'human_needed',
|
||||
category: 'human_uat',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// gaps_found items are already handled by plan-phase --gaps pipeline
|
||||
return items;
|
||||
}
|
||||
|
||||
function categorizeItem(result, reason, blockedBy) {
|
||||
if (result === 'blocked' || blockedBy) {
|
||||
if (blockedBy) {
|
||||
if (/server/i.test(blockedBy)) return 'server_blocked';
|
||||
if (/device|physical/i.test(blockedBy)) return 'device_needed';
|
||||
if (/build|release|preview/i.test(blockedBy)) return 'build_needed';
|
||||
if (/third.party|twilio|stripe/i.test(blockedBy)) return 'third_party';
|
||||
}
|
||||
return 'blocked';
|
||||
}
|
||||
if (result === 'skipped') {
|
||||
if (reason) {
|
||||
if (/server|not running|not available/i.test(reason)) return 'server_blocked';
|
||||
if (/simulator|physical|device/i.test(reason)) return 'device_needed';
|
||||
if (/build|release|preview/i.test(reason)) return 'build_needed';
|
||||
}
|
||||
return 'skipped_unresolved';
|
||||
}
|
||||
if (result === 'pending') return 'pending';
|
||||
if (result === 'human_needed') return 'human_uat';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
module.exports = { cmdAuditUat };
|
||||
@@ -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,
|
||||
|
||||
@@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
|
||||
<task type="auto">
|
||||
<name>Start dev server for verification</name>
|
||||
<action>Run `npm run dev` in background, wait for "ready" message, capture port</action>
|
||||
<verify>curl http://localhost:3000 returns 200</verify>
|
||||
<verify>fetch http://localhost:3000 returns 200</verify>
|
||||
<done>Dev server running at http://localhost:3000</done>
|
||||
</task>
|
||||
|
||||
@@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
|
||||
<name>Deploy to Vercel</name>
|
||||
<files>.vercel/, vercel.json</files>
|
||||
<action>Run `vercel --yes` to deploy</action>
|
||||
<verify>vercel ls shows deployment, curl returns 200</verify>
|
||||
<verify>vercel ls shows deployment, fetch returns 200</verify>
|
||||
</task>
|
||||
|
||||
<!-- If vercel returns "Error: Not authenticated", Claude creates checkpoint on the fly -->
|
||||
@@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
|
||||
<task type="auto">
|
||||
<name>Retry Vercel deployment</name>
|
||||
<action>Run `vercel --yes` (now authenticated)</action>
|
||||
<verify>vercel ls shows deployment, curl returns 200</verify>
|
||||
<verify>vercel ls shows deployment, fetch returns 200</verify>
|
||||
</task>
|
||||
```
|
||||
|
||||
@@ -455,8 +455,8 @@ I'll verify: vercel whoami returns your account
|
||||
npm run dev &
|
||||
DEV_SERVER_PID=$!
|
||||
|
||||
# Wait for ready (max 30s)
|
||||
timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done'
|
||||
# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility
|
||||
timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done'
|
||||
```
|
||||
|
||||
**Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`).
|
||||
@@ -489,7 +489,9 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
|
||||
| Auth error | Create auth gate checkpoint |
|
||||
| Network timeout | Retry with backoff, then checkpoint if persistent |
|
||||
|
||||
**Never present a checkpoint with broken verification environment.** If `curl localhost:3000` fails, don't ask user to "visit localhost:3000".
|
||||
**Never present a checkpoint with broken verification environment.** If the local server isn't responding, don't ask user to "visit localhost:3000".
|
||||
|
||||
> **Cross-platform note:** Use `node -e "fetch('http://localhost:3000').then(r=>console.log(r.status))"` instead of `curl` for health checks. `curl` is broken on Windows MSYS/Git Bash due to SSL/path mangling issues.
|
||||
|
||||
```xml
|
||||
<!-- WRONG: Checkpoint with broken environment -->
|
||||
@@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
|
||||
<task type="auto">
|
||||
<name>Fix server startup issue</name>
|
||||
<action>Investigate error, fix root cause, restart server</action>
|
||||
<verify>curl http://localhost:3000 returns 200</verify>
|
||||
<verify>fetch http://localhost:3000 returns 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify">
|
||||
@@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
|
||||
<task type="auto">
|
||||
<name>Start dev server for auth testing</name>
|
||||
<action>Run `npm run dev` in background, wait for ready signal</action>
|
||||
<verify>curl http://localhost:3000 returns 200</verify>
|
||||
<verify>fetch http://localhost:3000 returns 200</verify>
|
||||
<done>Dev server running at http://localhost:3000</done>
|
||||
</task>
|
||||
|
||||
@@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
|
||||
<task type="auto">
|
||||
<name>Start dev server</name>
|
||||
<action>Run `npm run dev` in background</action>
|
||||
<verify>curl localhost:3000 returns 200</verify>
|
||||
<verify>fetch http://localhost:3000 returns 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
@@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
|
||||
<task type="auto">
|
||||
<name>Deploy to Vercel</name>
|
||||
<action>Run `vercel --yes`. Capture URL.</action>
|
||||
<verify>vercel ls shows deployment, curl returns 200</verify>
|
||||
<verify>vercel ls shows deployment, fetch returns 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify">
|
||||
|
||||
@@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p
|
||||
|
||||
Each task gets its own commit immediately after completion.
|
||||
|
||||
> **Parallel agents:** When running as a parallel executor (spawned by execute-phase),
|
||||
> use `--no-verify` on all commits to avoid pre-commit hook lock contention.
|
||||
> The orchestrator validates hooks once after all agents complete.
|
||||
|
||||
```
|
||||
{type}({phase}-{plan}): {task-name}
|
||||
|
||||
@@ -246,3 +250,46 @@ Each plan produces 2-4 commits (tasks + metadata). Clear, granular, bisectable.
|
||||
- "Commit noise" irrelevant when consumer is Claude, not humans
|
||||
|
||||
</commit_strategy_rationale>
|
||||
|
||||
<sub_repos_support>
|
||||
|
||||
## Multi-Repo Workspace Support (sub_repos)
|
||||
|
||||
For workspaces with separate git repos (e.g., `backend/`, `frontend/`, `shared/`), GSD routes commits to each repo independently.
|
||||
|
||||
### Configuration
|
||||
|
||||
In `.planning/config.json`, list sub-repo directories under `planning.sub_repos`:
|
||||
|
||||
```json
|
||||
{
|
||||
"planning": {
|
||||
"commit_docs": false,
|
||||
"sub_repos": ["backend", "frontend", "shared"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `commit_docs: false` so planning docs stay local and are not committed to any sub-repo.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk.
|
||||
2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo).
|
||||
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
|
||||
4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.
|
||||
|
||||
### Commit Routing
|
||||
|
||||
Instead of the standard `commit` command, use `commit-to-subrepo` when `sub_repos` is configured:
|
||||
|
||||
```bash
|
||||
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "feat(02-01): add user API" \
|
||||
--files backend/src/api/users.ts backend/src/types/user.ts frontend/src/components/UserForm.tsx
|
||||
```
|
||||
|
||||
This stages `src/api/users.ts` and `src/types/user.ts` in the `backend/` repo, and `src/components/UserForm.tsx` in the `frontend/` repo, then commits each independently with the same message.
|
||||
|
||||
Files that don't match any configured sub-repo are reported as unmatched.
|
||||
|
||||
</sub_repos_support>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -11,7 +11,8 @@ Configuration options for `.planning/` directory behavior.
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}"
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
||||
"quick_branch_template": null
|
||||
}
|
||||
```
|
||||
|
||||
@@ -22,6 +23,7 @@ Configuration options for `.planning/` directory behavior.
|
||||
| `git.branching_strategy` | `"none"` | Git branching approach: `"none"`, `"phase"`, or `"milestone"` |
|
||||
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy |
|
||||
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy |
|
||||
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
|
||||
</config_schema>
|
||||
|
||||
<commit_docs_behavior>
|
||||
|
||||
@@ -8,7 +8,7 @@ Template for `.planning/phases/XX-name/{phase_num}-UAT.md` — persistent UAT se
|
||||
|
||||
```markdown
|
||||
---
|
||||
status: testing | complete | diagnosed
|
||||
status: testing | partial | complete | diagnosed
|
||||
phase: XX-name
|
||||
source: [list of SUMMARY.md files tested]
|
||||
started: [ISO timestamp]
|
||||
@@ -45,6 +45,12 @@ expected: [observable behavior]
|
||||
result: skipped
|
||||
reason: [why skipped]
|
||||
|
||||
### 5. [Test Name]
|
||||
expected: [observable behavior]
|
||||
result: blocked
|
||||
blocked_by: server | physical-device | release-build | third-party | prior-phase
|
||||
reason: [why blocked]
|
||||
|
||||
...
|
||||
|
||||
## Summary
|
||||
@@ -54,6 +60,7 @@ passed: [N]
|
||||
issues: [N]
|
||||
pending: [N]
|
||||
skipped: [N]
|
||||
blocked: [N]
|
||||
|
||||
## Gaps
|
||||
|
||||
@@ -74,7 +81,7 @@ skipped: [N]
|
||||
<section_rules>
|
||||
|
||||
**Frontmatter:**
|
||||
- `status`: OVERWRITE - "testing" or "complete"
|
||||
- `status`: OVERWRITE - "testing", "partial", or "complete"
|
||||
- `phase`: IMMUTABLE - set on creation
|
||||
- `source`: IMMUTABLE - SUMMARY files being tested
|
||||
- `started`: IMMUTABLE - set on creation
|
||||
@@ -87,9 +94,10 @@ skipped: [N]
|
||||
|
||||
**Tests:**
|
||||
- Each test: OVERWRITE result field when user responds
|
||||
- `result` values: [pending], pass, issue, skipped
|
||||
- `result` values: [pending], pass, issue, skipped, blocked
|
||||
- If issue: add `reported` (verbatim) and `severity` (inferred)
|
||||
- If skipped: add `reason` if provided
|
||||
- If blocked: add `blocked_by` (tag) and `reason` (if provided)
|
||||
|
||||
**Summary:**
|
||||
- OVERWRITE counts after each response
|
||||
@@ -156,6 +164,16 @@ skipped: [N]
|
||||
- Commit file
|
||||
- Present summary with next steps
|
||||
|
||||
**Partial completion:**
|
||||
- status → "partial" (if pending, blocked, or unresolved skipped tests remain)
|
||||
- Current Test → "[testing paused — {N} items outstanding]"
|
||||
- Commit file
|
||||
- Present summary with outstanding items highlighted
|
||||
|
||||
**Resuming partial session:**
|
||||
- `/gsd:verify-work {phase}` picks up from first pending/blocked test
|
||||
- When all items resolved, status advances to "complete"
|
||||
|
||||
**Resume after /clear:**
|
||||
1. Read frontmatter → know phase and status
|
||||
2. Read Current Test → know where we are
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
Template for project-root `CLAUDE.md` — auto-generated by `gsd-tools generate-claude-md`.
|
||||
|
||||
Contains 5 marker-bounded sections. Each section is independently updatable.
|
||||
The `generate-claude-md` subcommand manages 4 sections (project, stack, conventions, architecture).
|
||||
Contains 6 marker-bounded sections. Each section is independently updatable.
|
||||
The `generate-claude-md` subcommand manages 5 sections (project, stack, conventions, architecture, workflow enforcement).
|
||||
The profile section is managed exclusively by `generate-claude-profile`.
|
||||
|
||||
---
|
||||
@@ -66,6 +66,22 @@ Conventions not yet established. Will populate as patterns emerge during develop
|
||||
Architecture not yet mapped. Follow existing patterns found in the codebase.
|
||||
```
|
||||
|
||||
### Workflow Enforcement Section
|
||||
```
|
||||
<!-- GSD:workflow-start source:GSD defaults -->
|
||||
## GSD Workflow Enforcement
|
||||
|
||||
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
||||
|
||||
Use these entry points:
|
||||
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
|
||||
- `/gsd:debug` for investigation and bug fixing
|
||||
- `/gsd:execute-phase` for planned phase work
|
||||
|
||||
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
||||
<!-- GSD:workflow-end -->
|
||||
```
|
||||
|
||||
### Profile Section (Placeholder Only)
|
||||
```
|
||||
<!-- GSD:profile-start -->
|
||||
@@ -88,7 +104,8 @@ CLAUDE.md file and no profile section exists yet.
|
||||
2. **Stack** — Technology choices (what tools are used)
|
||||
3. **Conventions** — Code patterns and rules (how code is written)
|
||||
4. **Architecture** — System structure (how components fit together)
|
||||
5. **Profile** — Developer behavioral preferences (how to interact)
|
||||
5. **Workflow Enforcement** — Default GSD entry points for file-changing work
|
||||
6. **Profile** — Developer behavioral preferences (how to interact)
|
||||
|
||||
## Marker Format
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"planning": {
|
||||
"commit_docs": true,
|
||||
"search_gitignored": false
|
||||
"search_gitignored": false,
|
||||
"sub_repos": []
|
||||
},
|
||||
"parallelization": {
|
||||
"enabled": true,
|
||||
|
||||
63
get-shit-done/templates/discussion-log.md
Normal file
63
get-shit-done/templates/discussion-log.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Discussion Log Template
|
||||
|
||||
Template for `.planning/phases/XX-name/{phase_num}-DISCUSSION-LOG.md` — audit trail of discuss-phase Q&A sessions.
|
||||
|
||||
**Purpose:** Software audit trail for decision-making. Captures all options considered, not just the selected one. Separate from CONTEXT.md which is the implementation artifact consumed by downstream agents.
|
||||
|
||||
**NOT for LLM consumption.** This file should never be referenced in `<files_to_read>` blocks or agent prompts.
|
||||
|
||||
## Format
|
||||
|
||||
```markdown
|
||||
# Phase [X]: [Name] - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** [ISO date]
|
||||
**Phase:** [phase number]-[phase name]
|
||||
**Areas discussed:** [comma-separated list]
|
||||
|
||||
---
|
||||
|
||||
## [Area 1 Name]
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| [Option 1] | [Brief description] | |
|
||||
| [Option 2] | [Brief description] | ✓ |
|
||||
| [Option 3] | [Brief description] | |
|
||||
|
||||
**User's choice:** [Selected option or verbatim free-text response]
|
||||
**Notes:** [Any clarifications or rationale provided during discussion]
|
||||
|
||||
---
|
||||
|
||||
## [Area 2 Name]
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
[Areas delegated to Claude's judgment — list what was deferred and why]
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
[Ideas mentioned but not in scope for this phase]
|
||||
|
||||
---
|
||||
|
||||
*Phase: XX-name*
|
||||
*Discussion log generated: [date]*
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Generated automatically at end of every discuss-phase session
|
||||
- Includes ALL options considered, not just the selected one
|
||||
- Includes user's freeform notes and clarifications
|
||||
- Clearly marked as audit-only, not an implementation artifact
|
||||
- Does NOT interfere with CONTEXT.md generation or downstream agent behavior
|
||||
- Committed alongside CONTEXT.md in the same git commit
|
||||
@@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components.
|
||||
<name>Task 2: Create User API endpoints</name>
|
||||
<files>src/features/user/api.ts</files>
|
||||
<action>GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model.</action>
|
||||
<verify>curl tests pass for all endpoints</verify>
|
||||
<verify>fetch tests pass for all endpoints</verify>
|
||||
<done>All CRUD operations work</done>
|
||||
</task>
|
||||
</tasks>
|
||||
@@ -407,7 +407,7 @@ Output: Working dashboard component.
|
||||
<task type="auto">
|
||||
<name>Start dev server</name>
|
||||
<action>Run `npm run dev` in background, wait for ready</action>
|
||||
<verify>curl localhost:3000 returns 200</verify>
|
||||
<verify>fetch http://localhost:3000 returns 200</verify>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
|
||||
@@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform
|
||||
<evolution>
|
||||
|
||||
PROJECT.md evolves throughout the project lifecycle.
|
||||
These rules are embedded in the generated PROJECT.md (## Evolution section)
|
||||
and implemented by workflows/transition.md and workflows/complete-milestone.md.
|
||||
|
||||
**After each phase transition:**
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
|
||||
109
get-shit-done/workflows/audit-uat.md
Normal file
109
get-shit-done/workflows/audit-uat.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<purpose>
|
||||
Cross-phase audit of all UAT and verification files. Finds every outstanding item (pending, skipped, blocked, human_needed), optionally verifies against the codebase to detect stale docs, and produces a prioritized human test plan.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="initialize">
|
||||
Run the CLI audit:
|
||||
|
||||
```bash
|
||||
AUDIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw)
|
||||
```
|
||||
|
||||
Parse JSON for `results` array and `summary` object.
|
||||
|
||||
If `summary.total_items` is 0:
|
||||
```
|
||||
## All Clear
|
||||
|
||||
No outstanding UAT or verification items found across all phases.
|
||||
All tests are passing, resolved, or diagnosed with fix plans.
|
||||
```
|
||||
Stop here.
|
||||
</step>
|
||||
|
||||
<step name="categorize">
|
||||
Group items by what's actionable NOW vs. what needs prerequisites:
|
||||
|
||||
**Testable Now** (no external dependencies):
|
||||
- `pending` — tests never run
|
||||
- `human_uat` — human verification items
|
||||
- `skipped_unresolved` — skipped without clear blocking reason
|
||||
|
||||
**Needs Prerequisites:**
|
||||
- `server_blocked` — needs external server running
|
||||
- `device_needed` — needs physical device (not simulator)
|
||||
- `build_needed` — needs release/preview build
|
||||
- `third_party` — needs external service configuration
|
||||
|
||||
For each item in "Testable Now", use Grep/Read to check if the underlying feature still exists in the codebase:
|
||||
- If the test references a component/function that no longer exists → mark as `stale`
|
||||
- If the test references code that has been significantly rewritten → mark as `needs_update`
|
||||
- Otherwise → mark as `active`
|
||||
</step>
|
||||
|
||||
<step name="present">
|
||||
Present the audit report:
|
||||
|
||||
```
|
||||
## UAT Audit Report
|
||||
|
||||
**{total_items} outstanding items across {total_files} files in {phase_count} phases**
|
||||
|
||||
### Testable Now ({count})
|
||||
|
||||
| # | Phase | Test | Description | Status |
|
||||
|---|-------|------|-------------|--------|
|
||||
| 1 | {phase} | {test_name} | {expected} | {active/stale/needs_update} |
|
||||
...
|
||||
|
||||
### Needs Prerequisites ({count})
|
||||
|
||||
| # | Phase | Test | Blocked By | Description |
|
||||
|---|-------|------|------------|-------------|
|
||||
| 1 | {phase} | {test_name} | {category} | {expected} |
|
||||
...
|
||||
|
||||
### Stale (can be closed) ({count})
|
||||
|
||||
| # | Phase | Test | Why Stale |
|
||||
|---|-------|------|-----------|
|
||||
| 1 | {phase} | {test_name} | {reason} |
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
1. **Close stale items:** `/gsd:verify-work {phase}` — mark stale tests as resolved
|
||||
2. **Run active tests:** Human UAT test plan below
|
||||
3. **When prerequisites met:** Retest blocked items with `/gsd:verify-work {phase}`
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="test_plan">
|
||||
Generate a human UAT test plan for "Testable Now" + "active" items only:
|
||||
|
||||
Group by what can be tested together (same screen, same feature, same prerequisite):
|
||||
|
||||
```
|
||||
## Human UAT Test Plan
|
||||
|
||||
### Group 1: {category — e.g., "Billing Flow"}
|
||||
Prerequisites: {what needs to be running/configured}
|
||||
|
||||
1. **{Test name}** (Phase {N})
|
||||
- Navigate to: {where}
|
||||
- Do: {action}
|
||||
- Expected: {expected behavior}
|
||||
|
||||
2. **{Test name}** (Phase {N})
|
||||
...
|
||||
|
||||
### Group 2: {category}
|
||||
...
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
@@ -110,6 +110,18 @@ Phase: "API documentation"
|
||||
1. Retry the question once with the same parameters
|
||||
2. If still empty, present the options as a plain-text numbered list and ask the user to type their choice number
|
||||
Never proceed with an empty answer.
|
||||
|
||||
**Text mode (`workflow.text_mode: true` in config or `--text` flag):**
|
||||
When text mode is active, **do not use AskUserQuestion at all**. Instead, present every
|
||||
question as a plain-text numbered list and ask the user to type their choice number.
|
||||
This is required for Claude Code remote sessions (`/rc` mode) where the Claude App
|
||||
cannot forward TUI menu selections back to the host.
|
||||
|
||||
Enable text mode:
|
||||
- Per-session: pass `--text` flag to any command (e.g., `/gsd:discuss-phase --text`)
|
||||
- Per-project: `gsd-tools config-set workflow.text_mode true`
|
||||
|
||||
Text mode applies to ALL workflows in the session, not just discuss-phase.
|
||||
</answer_validation>
|
||||
|
||||
<process>
|
||||
@@ -242,6 +254,47 @@ Structure the extracted information:
|
||||
**If no prior context exists:** Continue without — this is expected for early phases.
|
||||
</step>
|
||||
|
||||
<step name="cross_reference_todos">
|
||||
Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed.
|
||||
|
||||
**Load and match todos:**
|
||||
```bash
|
||||
TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}")
|
||||
```
|
||||
|
||||
Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`).
|
||||
|
||||
**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown.
|
||||
|
||||
**If matches found:**
|
||||
|
||||
Present matched todos to the user. Show each match with its title, area, and why it matched:
|
||||
|
||||
```
|
||||
📋 Found {N} pending todo(s) that may be relevant to Phase {X}:
|
||||
|
||||
{For each match:}
|
||||
- **{title}** (area: {area}, relevance: {score}) — matched on {reasons}
|
||||
```
|
||||
|
||||
Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope:
|
||||
|
||||
```
|
||||
Which of these todos should be folded into Phase {X} scope?
|
||||
(Select any that apply, or none to skip)
|
||||
```
|
||||
|
||||
**For selected (folded) todos:**
|
||||
- Store internally as `<folded_todos>` for inclusion in CONTEXT.md `<decisions>` section
|
||||
- These become additional scope items that downstream agents (researcher, planner) will see
|
||||
|
||||
**For unselected (reviewed but not folded) todos:**
|
||||
- Store internally as `<reviewed_todos>` for inclusion in CONTEXT.md `<deferred>` section
|
||||
- This prevents future phases from re-surfacing the same todos as "missed"
|
||||
|
||||
**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection.
|
||||
</step>
|
||||
|
||||
<step name="scout_codebase">
|
||||
Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session.
|
||||
|
||||
@@ -404,8 +457,58 @@ Continue to discuss_areas with selected areas.
|
||||
<step name="discuss_areas">
|
||||
For each selected area, conduct a focused discussion loop.
|
||||
|
||||
**Research-before-questions mode:** Check if `research_questions` is enabled in config (from init context or `.planning/config.json`). When enabled, before presenting questions for each area:
|
||||
1. Do a brief web search for best practices related to the area topic
|
||||
2. Summarize the top findings in 2-3 bullet points
|
||||
3. Present the research alongside the question so the user can make a more informed decision
|
||||
|
||||
Example with research enabled:
|
||||
```
|
||||
Let's talk about [Authentication Strategy].
|
||||
|
||||
📊 Best practices research:
|
||||
• OAuth 2.0 + PKCE is the current standard for SPAs (replaces implicit flow)
|
||||
• Session tokens with httpOnly cookies preferred over localStorage for XSS protection
|
||||
• Consider passkey/WebAuthn support — adoption is accelerating in 2025-2026
|
||||
|
||||
With that context: How should users authenticate?
|
||||
```
|
||||
|
||||
When disabled (default), skip the research and present questions directly as before.
|
||||
|
||||
**Text mode support:** Parse optional `--text` from `$ARGUMENTS`.
|
||||
- Accept `--text` flag OR read `workflow.text_mode` from config (from init context)
|
||||
- When active, replace ALL `AskUserQuestion` calls with plain-text numbered lists
|
||||
- User types a number to select, or types free text for "Other"
|
||||
- This is required for Claude Code remote sessions (`/rc` mode) where TUI menus
|
||||
don't work through the Claude App
|
||||
|
||||
**Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`.
|
||||
- Accept `--batch`, `--batch=N`, or `--batch N`
|
||||
|
||||
**Analyze mode support:** Parse optional `--analyze` from `$ARGUMENTS`.
|
||||
When `--analyze` is active, before presenting each question (or question group in batch mode), provide a brief **trade-off analysis** for the decision:
|
||||
- 2-3 options with pros/cons based on codebase context and common patterns
|
||||
- A recommended approach with reasoning
|
||||
- Known pitfalls or constraints from prior phases
|
||||
|
||||
Example with `--analyze`:
|
||||
```
|
||||
**Trade-off analysis: Authentication strategy**
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Session cookies | Simple, httpOnly prevents XSS | Requires CSRF protection, sticky sessions |
|
||||
| JWT (stateless) | Scalable, no server state | Token size, revocation complexity |
|
||||
| OAuth 2.0 + PKCE | Industry standard for SPAs | More setup, redirect flow UX |
|
||||
|
||||
💡 Recommended: OAuth 2.0 + PKCE — your app has social login in requirements (REQ-04) and this aligns with the existing NextAuth setup in `src/lib/auth.ts`.
|
||||
|
||||
How should users authenticate?
|
||||
```
|
||||
|
||||
This gives the user context to make informed decisions without extra prompting. When `--analyze` is absent, present questions directly as before.
|
||||
- Accept `--batch`, `--batch=N`, or `--batch N`
|
||||
- Default to 4 questions per batch when no number is provided
|
||||
- Clamp explicit sizes to 2-5 so a batch stays answerable
|
||||
- If `--batch` is absent, keep the existing one-question-at-a-time flow
|
||||
@@ -500,11 +603,23 @@ Back to [current area]: [return to current question]"
|
||||
```
|
||||
|
||||
Track deferred ideas internally.
|
||||
|
||||
**Track discussion log data internally:**
|
||||
For each question asked, accumulate:
|
||||
- Area name
|
||||
- All options presented (label + description)
|
||||
- Which option the user selected (or their free-text response)
|
||||
- Any follow-up notes or clarifications the user provided
|
||||
This data is used to generate DISCUSSION-LOG.md in the `write_context` step.
|
||||
</step>
|
||||
|
||||
<step name="write_context">
|
||||
Create CONTEXT.md capturing decisions made.
|
||||
|
||||
**Also generate DISCUSSION-LOG.md** — a full audit trail of the discuss-phase Q&A.
|
||||
This file is for human reference only (software audits, compliance reviews). It is NOT
|
||||
consumed by downstream agents (researcher, planner, executor).
|
||||
|
||||
**Find or create phase directory:**
|
||||
|
||||
Use values from init: `phase_dir`, `phase_slug`, `padded_phase`.
|
||||
@@ -544,6 +659,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
|
||||
### Claude's Discretion
|
||||
[Areas where user said "you decide" — note that Claude has flexibility here]
|
||||
|
||||
### Folded Todos
|
||||
[If any todos were folded into scope from the cross_reference_todos step, list them here.
|
||||
Each entry should include the todo title, original problem, and how it fits this phase's scope.
|
||||
If no todos were folded: omit this subsection entirely.]
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
@@ -595,6 +715,12 @@ Every entry needs a full relative path — not just a name.]
|
||||
|
||||
[Ideas that came up but belong in other phases. Don't lose them.]
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
[If any todos were reviewed in cross_reference_todos but not folded into scope,
|
||||
list them here so future phases know they were considered.
|
||||
Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.)
|
||||
If no reviewed-but-deferred todos: omit this subsection entirely.]
|
||||
|
||||
[If none: "None — discussion stayed within phase scope"]
|
||||
|
||||
</deferred>
|
||||
@@ -648,10 +774,54 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md
|
||||
</step>
|
||||
|
||||
<step name="git_commit">
|
||||
Commit phase context (uses `commit_docs` from init internally):
|
||||
**Write DISCUSSION-LOG.md before committing:**
|
||||
|
||||
**File location:** `${phase_dir}/${padded_phase}-DISCUSSION-LOG.md`
|
||||
|
||||
```markdown
|
||||
# Phase [X]: [Name] - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** [ISO date]
|
||||
**Phase:** [phase number]-[phase name]
|
||||
**Areas discussed:** [comma-separated list]
|
||||
|
||||
---
|
||||
|
||||
[For each gray area discussed:]
|
||||
|
||||
## [Area Name]
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| [Option 1] | [Description from AskUserQuestion] | |
|
||||
| [Option 2] | [Description] | ✓ |
|
||||
| [Option 3] | [Description] | |
|
||||
|
||||
**User's choice:** [Selected option or free-text response]
|
||||
**Notes:** [Any clarifications, follow-up context, or rationale the user provided]
|
||||
|
||||
---
|
||||
|
||||
[Repeat for each area]
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
[List areas where user said "you decide" or deferred to Claude]
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
[Ideas mentioned during discussion that were noted for future phases]
|
||||
```
|
||||
|
||||
Write file.
|
||||
|
||||
Commit phase context and discussion log:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
|
||||
```
|
||||
|
||||
Confirm: "Committed: docs(${padded_phase}): capture phase context"
|
||||
|
||||
@@ -6,10 +6,45 @@ Execute all plans in a phase using wave-based parallel execution. Orchestrator s
|
||||
Orchestrator coordinates, not executes. Each subagent loads the full execute-plan context. Orchestrator: discover plans → analyze deps → group waves → spawn agents → handle checkpoints → collect results.
|
||||
</core_principle>
|
||||
|
||||
<runtime_compatibility>
|
||||
**Subagent spawning is runtime-specific:**
|
||||
- **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result
|
||||
- **Copilot:** Subagent spawning does not reliably return completion signals. **Default to
|
||||
sequential inline execution**: read and follow execute-plan.md directly for each plan
|
||||
instead of spawning parallel agents. Only attempt parallel spawning if the user
|
||||
explicitly requests it — and in that case, rely on the spot-check fallback in step 3
|
||||
to detect completion.
|
||||
- **Other runtimes (Gemini, Codex, OpenCode):** If Task/subagent API is unavailable, use sequential
|
||||
inline execution as the fallback.
|
||||
|
||||
**Fallback rule:** If a spawned agent completes its work (commits visible, SUMMARY.md exists) but
|
||||
the orchestrator never receives the completion signal, treat it as successful based on spot-checks
|
||||
and continue to the next wave/plan. Never block indefinitely waiting for a signal — always verify
|
||||
via filesystem and git state.
|
||||
</runtime_compatibility>
|
||||
|
||||
<required_reading>
|
||||
Read STATE.md before any operation to load project context.
|
||||
</required_reading>
|
||||
|
||||
<available_agent_types>
|
||||
These are the valid GSD subagent types registered in .claude/agents/ (or equivalent for your runtime).
|
||||
Always use the exact name from this list — do not fall back to 'general-purpose' or other built-in types:
|
||||
|
||||
- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md
|
||||
- gsd-verifier — Verifies phase completion, checks quality gates
|
||||
- gsd-planner — Creates detailed plans from phase scope
|
||||
- gsd-phase-researcher — Researches technical approaches for a phase
|
||||
- gsd-plan-checker — Reviews plan quality before execution
|
||||
- gsd-debugger — Diagnoses and fixes issues
|
||||
- gsd-codebase-mapper — Maps project structure and dependencies
|
||||
- gsd-integration-checker — Checks cross-phase integration
|
||||
- gsd-nyquist-auditor — Validates verification coverage
|
||||
- gsd-ui-researcher — Researches UI/UX approaches
|
||||
- gsd-ui-checker — Reviews UI implementation quality
|
||||
- gsd-ui-auditor — Audits UI against design requirements
|
||||
</available_agent_types>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="initialize" priority="first">
|
||||
@@ -28,6 +63,14 @@ Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelizat
|
||||
|
||||
When `parallelization` is false, plans within a wave execute sequentially.
|
||||
|
||||
**Runtime detection for Copilot:**
|
||||
Check if the current runtime is Copilot by testing for the `@gsd-executor` agent pattern
|
||||
or absence of the `Task()` subagent API. If running under Copilot, force sequential inline
|
||||
execution regardless of the `parallelization` setting — Copilot's subagent completion
|
||||
signals are unreliable (see `<runtime_compatibility>`). Set `COPILOT_SEQUENTIAL=true`
|
||||
internally and skip the `execute_waves` step in favor of `check_interactive_mode`'s
|
||||
inline path for each plan.
|
||||
|
||||
**REQUIRED — Sync chain flag with intent.** If user invoked manually (no `--auto`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This prevents stale `_auto_chain_active: true` from causing unwanted auto-advance. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference). You MUST execute this bash block before any config reads:
|
||||
```bash
|
||||
# REQUIRED: prevents stale auto-chain from previous --auto runs
|
||||
@@ -37,6 +80,54 @@ fi
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="check_interactive_mode">
|
||||
**Parse `--interactive` flag from $ARGUMENTS.**
|
||||
|
||||
**If `--interactive` flag present:** Switch to interactive execution mode.
|
||||
|
||||
Interactive mode executes plans sequentially **inline** (no subagent spawning) with user
|
||||
checkpoints between tasks. The user can review, modify, or redirect work at any point.
|
||||
|
||||
**Interactive execution flow:**
|
||||
|
||||
1. Load plan inventory as normal (discover_and_group_plans)
|
||||
2. For each plan (sequentially, ignoring wave grouping):
|
||||
|
||||
a. **Present the plan to the user:**
|
||||
```
|
||||
## Plan {plan_id}: {plan_name}
|
||||
|
||||
Objective: {from plan file}
|
||||
Tasks: {task_count}
|
||||
|
||||
Options:
|
||||
- Execute (proceed with all tasks)
|
||||
- Review first (show task breakdown before starting)
|
||||
- Skip (move to next plan)
|
||||
- Stop (end execution, save progress)
|
||||
```
|
||||
|
||||
b. **If "Review first":** Read and display the full plan file. Ask again: Execute, Modify, Skip.
|
||||
|
||||
c. **If "Execute":** Read and follow `~/.claude/get-shit-done/workflows/execute-plan.md` **inline**
|
||||
(do NOT spawn a subagent). Execute tasks one at a time.
|
||||
|
||||
d. **After each task:** Pause briefly. If the user intervenes (types anything), stop and address
|
||||
their feedback before continuing. Otherwise proceed to next task.
|
||||
|
||||
e. **After plan complete:** Show results, commit, create SUMMARY.md, then present next plan.
|
||||
|
||||
3. After all plans: proceed to verification (same as normal mode).
|
||||
|
||||
**Benefits of interactive mode:**
|
||||
- No subagent overhead — dramatically lower token usage
|
||||
- User catches mistakes early — saves costly verification cycles
|
||||
- Maintains GSD's planning/tracking structure
|
||||
- Best for: small phases, bug fixes, verification gaps, learning GSD
|
||||
|
||||
**Skip to handle_branching step** (interactive plans execute inline after grouping).
|
||||
</step>
|
||||
|
||||
<step name="handle_branching">
|
||||
Check `branching_strategy` from init:
|
||||
|
||||
@@ -111,8 +202,9 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
|
||||
2. **Spawn executor agents:**
|
||||
|
||||
Pass paths only — executors read files themselves with their fresh 200k context.
|
||||
This keeps orchestrator context lean (~10-15%).
|
||||
Pass paths only — executors read files themselves with their fresh context window.
|
||||
For 200k models, this keeps orchestrator context lean (~10-15%).
|
||||
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
|
||||
|
||||
```
|
||||
Task(
|
||||
@@ -124,6 +216,14 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md.
|
||||
</objective>
|
||||
|
||||
<parallel_execution>
|
||||
You are running as a PARALLEL executor agent. Use --no-verify on all git
|
||||
commits to avoid pre-commit hook contention with other agents. The
|
||||
orchestrator validates hooks once after all agents complete.
|
||||
For gsd-tools commits: add --no-verify flag.
|
||||
For direct git commits: use git commit --no-verify -m "..."
|
||||
</parallel_execution>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@~/.claude/get-shit-done/templates/summary.md
|
||||
@@ -134,12 +234,20 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
<files_to_read>
|
||||
Read these files at execution start using the Read tool:
|
||||
- {phase_dir}/{plan_file} (Plan)
|
||||
- .planning/PROJECT.md (Project context — core value, requirements, evolution rules)
|
||||
- .planning/STATE.md (State)
|
||||
- .planning/config.json (Config, if exists)
|
||||
- ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions)
|
||||
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
|
||||
</files_to_read>
|
||||
|
||||
<mcp_tools>
|
||||
If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7,
|
||||
or other MCP servers), prefer those tools over Grep/Glob for code navigation when available.
|
||||
MCP tools often save significant tokens by providing structured code indexes.
|
||||
Check tool availability first — if MCP tools are not accessible, fall back to Grep/Glob.
|
||||
</mcp_tools>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] All tasks executed
|
||||
- [ ] Each task committed individually
|
||||
@@ -153,7 +261,39 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
|
||||
3. **Wait for all agents in wave to complete.**
|
||||
|
||||
4. **Report completion — spot-check claims first:**
|
||||
**Completion signal fallback (Copilot and runtimes where Task() may not return):**
|
||||
|
||||
If a spawned agent does not return a completion signal but appears to have finished
|
||||
its work, do NOT block indefinitely. Instead, verify completion via spot-checks:
|
||||
|
||||
```bash
|
||||
# For each plan in this wave, check if the executor finished:
|
||||
SUMMARY_EXISTS=$(test -f "{phase_dir}/{plan_number}-{plan_padded}-SUMMARY.md" && echo "true" || echo "false")
|
||||
COMMITS_FOUND=$(git log --oneline --all --grep="{phase_number}-{plan_padded}" --since="1 hour ago" | head -1)
|
||||
```
|
||||
|
||||
**If SUMMARY.md exists AND commits are found:** The agent completed successfully —
|
||||
treat as done and proceed to step 4. Log: `"✓ {Plan ID} completed (verified via spot-check — completion signal not received)"`
|
||||
|
||||
**If SUMMARY.md does NOT exist after a reasonable wait:** The agent may still be
|
||||
running or may have failed silently. Check `git log --oneline -5` for recent
|
||||
activity. If commits are still appearing, wait longer. If no activity, report
|
||||
the plan as failed and route to the failure handler in step 5.
|
||||
|
||||
**This fallback applies automatically to all runtimes.** Claude Code's Task() normally
|
||||
returns synchronously, but the fallback ensures resilience if it doesn't.
|
||||
|
||||
4. **Post-wave hook validation (parallel mode only):**
|
||||
|
||||
When agents committed with `--no-verify`, run pre-commit hooks once after the wave:
|
||||
```bash
|
||||
# Run project's pre-commit hooks on the current state
|
||||
git diff --cached --quiet || git stash # stash any unstaged changes
|
||||
git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing"
|
||||
```
|
||||
If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?"
|
||||
|
||||
5. **Report completion — spot-check claims first:**
|
||||
|
||||
For each SUMMARY.md:
|
||||
- Verify first 2 files from `key-files.created` exist on disk
|
||||
@@ -328,6 +468,67 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PARENT
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="regression_gate">
|
||||
Run prior phases' test suites to catch cross-phase regressions BEFORE verification.
|
||||
|
||||
**Skip if:** This is the first phase (no prior phases), or no prior VERIFICATION.md files exist.
|
||||
|
||||
**Step 1: Discover prior phases' test files**
|
||||
```bash
|
||||
# Find all VERIFICATION.md files from prior phases in current milestone
|
||||
PRIOR_VERIFICATIONS=$(find .planning/phases/ -name "*-VERIFICATION.md" ! -path "*${PHASE_NUMBER}*" 2>/dev/null)
|
||||
```
|
||||
|
||||
**Step 2: Extract test file lists from prior verifications**
|
||||
|
||||
For each VERIFICATION.md found, look for test file references:
|
||||
- Lines containing `test`, `spec`, or `__tests__` paths
|
||||
- The "Test Suite" or "Automated Checks" section
|
||||
- File patterns from `key-files.created` in corresponding SUMMARY.md files that match `*.test.*` or `*.spec.*`
|
||||
|
||||
Collect all unique test file paths into `REGRESSION_FILES`.
|
||||
|
||||
**Step 3: Run regression tests (if any found)**
|
||||
|
||||
```bash
|
||||
# Detect test runner and run prior phase tests
|
||||
if [ -f "package.json" ]; then
|
||||
# Node.js — use project's test runner
|
||||
npx jest ${REGRESSION_FILES} --passWithNoTests --no-coverage -q 2>&1 || npx vitest run ${REGRESSION_FILES} 2>&1
|
||||
elif [ -f "Cargo.toml" ]; then
|
||||
cargo test 2>&1
|
||||
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
|
||||
python -m pytest ${REGRESSION_FILES} -q --tb=short 2>&1
|
||||
fi
|
||||
```
|
||||
|
||||
**Step 4: Report results**
|
||||
|
||||
If all tests pass:
|
||||
```
|
||||
✓ Regression gate: {N} prior-phase test files passed — no regressions detected
|
||||
```
|
||||
→ Proceed to verify_phase_goal
|
||||
|
||||
If any tests fail:
|
||||
```
|
||||
## ⚠ Cross-Phase Regression Detected
|
||||
|
||||
Phase {X} execution may have broken functionality from prior phases.
|
||||
|
||||
| Test File | Phase | Status | Detail |
|
||||
|-----------|-------|--------|--------|
|
||||
| {file} | {origin_phase} | FAILED | {first_failure_line} |
|
||||
|
||||
Options:
|
||||
1. Fix regressions before verification (recommended)
|
||||
2. Continue to verification anyway (regressions will compound)
|
||||
3. Abort phase — roll back and re-plan
|
||||
```
|
||||
|
||||
Use AskUserQuestion to present the options.
|
||||
</step>
|
||||
|
||||
<step name="verify_phase_goal">
|
||||
Verify phase achieved its GOAL, not just completed tasks.
|
||||
|
||||
@@ -357,6 +558,51 @@ grep "^status:" "$PHASE_DIR"/*-VERIFICATION.md | cut -d: -f2 | tr -d ' '
|
||||
| `gaps_found` | Present gap summary, offer `/gsd:plan-phase {phase} --gaps` |
|
||||
|
||||
**If human_needed:**
|
||||
|
||||
**Step A: Persist human verification items as UAT file.**
|
||||
|
||||
Create `{phase_dir}/{phase_num}-HUMAN-UAT.md` using UAT template format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
status: partial
|
||||
phase: {phase_num}-{phase_name}
|
||||
source: [{phase_num}-VERIFICATION.md]
|
||||
started: [now ISO]
|
||||
updated: [now ISO]
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
[awaiting human testing]
|
||||
|
||||
## Tests
|
||||
|
||||
{For each human_verification item from VERIFICATION.md:}
|
||||
|
||||
### {N}. {item description}
|
||||
expected: {expected behavior from VERIFICATION.md}
|
||||
result: [pending]
|
||||
|
||||
## Summary
|
||||
|
||||
total: {count}
|
||||
passed: 0
|
||||
issues: 0
|
||||
pending: {count}
|
||||
skipped: 0
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
```
|
||||
|
||||
Commit the file:
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "test({phase_num}): persist human verification items as UAT" --files "{phase_dir}/{phase_num}-HUMAN-UAT.md"
|
||||
```
|
||||
|
||||
**Step B: Present to user:**
|
||||
|
||||
```
|
||||
## ✓ Phase {X}: {Name} — Human Verification Required
|
||||
|
||||
@@ -364,9 +610,15 @@ All automated checks passed. {N} items need human testing:
|
||||
|
||||
{From VERIFICATION.md human_verification section}
|
||||
|
||||
Items saved to `{phase_num}-HUMAN-UAT.md` — they will appear in `/gsd:progress` and `/gsd:audit-uat`.
|
||||
|
||||
"approved" → continue | Report issues → gap closure
|
||||
```
|
||||
|
||||
**If user says "approved":** Proceed to `update_roadmap`. The HUMAN-UAT.md file persists with `status: partial` and will surface in future progress checks until the user runs `/gsd:verify-work` on it.
|
||||
|
||||
**If user reports issues:** Proceed to gap closure as currently implemented.
|
||||
|
||||
**If gaps_found:**
|
||||
```
|
||||
## ⚠ Phase {X}: {Name} — Gaps Found
|
||||
@@ -404,14 +656,46 @@ The CLI handles:
|
||||
- Updating plan count to final
|
||||
- Advancing STATE.md to next phase
|
||||
- Updating REQUIREMENTS.md traceability
|
||||
- Scanning for verification debt (returns `warnings` array)
|
||||
|
||||
Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`.
|
||||
Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`, `warnings`, `has_warnings`.
|
||||
|
||||
**If has_warnings is true:**
|
||||
```
|
||||
## Phase {X} marked complete with {N} warnings:
|
||||
|
||||
{list each warning}
|
||||
|
||||
These items are tracked and will appear in `/gsd:progress` and `/gsd:audit-uat`.
|
||||
```
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): complete phase execution" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="update_project_md">
|
||||
**Evolve PROJECT.md to reflect phase completion (prevents planning document drift — #956):**
|
||||
|
||||
PROJECT.md tracks validated requirements, decisions, and current state. Without this step,
|
||||
PROJECT.md falls behind silently over multiple phases.
|
||||
|
||||
1. Read `.planning/PROJECT.md`
|
||||
2. If the file exists and has a `## Validated Requirements` or `## Requirements` section:
|
||||
- Move any requirements validated by this phase from Active → Validated
|
||||
- Add a brief note: `Validated in Phase {X}: {Name}`
|
||||
3. If the file has a `## Current State` or similar section:
|
||||
- Update it to reflect this phase's completion (e.g., "Phase {X} complete — {one-liner}")
|
||||
4. Update the `Last updated:` footer to today's date
|
||||
5. Commit the change:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md
|
||||
```
|
||||
|
||||
**Skip this step if** `.planning/PROJECT.md` does not exist.
|
||||
</step>
|
||||
|
||||
<step name="offer_next">
|
||||
|
||||
**Exception:** If `gaps_found`, the `verify_phase_goal` step already presents the gap-closure path (`/gsd:plan-phase {X} --gaps`). No additional routing needed — skip auto-advance.
|
||||
@@ -465,6 +749,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
|
||||
|
||||
**STOP. Do not auto-advance. Do not execute transition. Do not plan next phase. Present options to the user and wait.**
|
||||
|
||||
**IMPORTANT: There is NO `/gsd:transition` command. Never suggest it. The transition workflow is internal only.**
|
||||
|
||||
```
|
||||
## ✓ Phase {X}: {Name} Complete
|
||||
|
||||
@@ -473,12 +759,20 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
|
||||
/gsd:plan-phase {next} — plan next phase
|
||||
/gsd:execute-phase {next} — execute next phase
|
||||
```
|
||||
|
||||
Only suggest the commands listed above. Do not invent or hallucinate command names.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<context_efficiency>
|
||||
Orchestrator: ~10-15% context. Subagents: fresh 200k each. No polling (Task blocks). No context bleed.
|
||||
Orchestrator: ~10-15% context for 200k windows, can use more for 1M+ windows.
|
||||
Subagents: fresh context each (200k-1M depending on model). No polling (Task blocks). No context bleed.
|
||||
|
||||
For 1M+ context models, consider:
|
||||
- Passing richer context (code snippets, dependency outputs) directly to executors instead of just file paths
|
||||
- Running small phases (≤3 plans, no dependencies) inline without subagent spawning overhead
|
||||
- Relaxing /clear recommendations — context rot onset is much further out with 5x window
|
||||
</context_efficiency>
|
||||
|
||||
<failure_handling>
|
||||
|
||||
@@ -19,7 +19,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.
|
||||
Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`.
|
||||
|
||||
If `.planning/` missing: error.
|
||||
</step>
|
||||
@@ -135,7 +135,8 @@ If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness
|
||||
Deviations are normal — handle via rules below.
|
||||
|
||||
1. Read @context files from prompt
|
||||
2. Per task:
|
||||
2. **MCP tools:** If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch for code navigation), prefer them over Grep/Glob when available. Fall back to Grep/Glob if MCP tools are not accessible.
|
||||
3. Per task:
|
||||
- **MANDATORY read_first gate:** If the task has a `<read_first>` field, you MUST read every listed file BEFORE making any edits. This is not optional. Do not skip files because you "already know" what's in them — read them. The read_first files establish ground truth for the task.
|
||||
- `type="auto"`: if `tdd="true"` → TDD execution. Implement with deviation rules + auth gates. Verify done criteria. Commit (see task_commit). Track hash for Summary.
|
||||
- `type="checkpoint:*"`: STOP → checkpoint_protocol → wait for user → continue only after confirmation.
|
||||
@@ -233,6 +234,10 @@ See `~/.claude/get-shit-done/references/tdd.md` for structure.
|
||||
|
||||
Your commits may trigger pre-commit hooks. Auto-fix hooks handle themselves transparently — files get fixed and re-staged automatically.
|
||||
|
||||
**If running as a parallel executor agent (spawned by execute-phase):**
|
||||
Use `--no-verify` on all commits. Pre-commit hooks cause build lock contention when multiple agents commit simultaneously (e.g., cargo lock fights in Rust projects). The orchestrator validates once after all agents complete.
|
||||
|
||||
**If running as the sole executor (sequential mode):**
|
||||
If a commit is BLOCKED by a hook:
|
||||
|
||||
1. The `git commit` command fails with hook error output
|
||||
@@ -240,9 +245,7 @@ If a commit is BLOCKED by a hook:
|
||||
3. Fix the issue (type error, lint violation, secret leak, etc.)
|
||||
4. `git add` the fixed files
|
||||
5. Retry the commit
|
||||
6. Do NOT use `--no-verify`
|
||||
|
||||
This is normal and expected. Budget 1-2 retry cycles per commit.
|
||||
6. Budget 1-2 retry cycles per commit
|
||||
</precommit_failure_handling>
|
||||
|
||||
<task_commit>
|
||||
@@ -273,6 +276,20 @@ git add src/types/user.ts
|
||||
|
||||
**4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.
|
||||
|
||||
<sub_repos_commit_flow>
|
||||
**Sub-repos mode:** If `sub_repos` is configured (non-empty array from init context), use `commit-to-subrepo` instead of standard git commit. This routes files to their correct sub-repo based on path prefix.
|
||||
|
||||
```bash
|
||||
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {description}" --files file1 file2 ...
|
||||
```
|
||||
|
||||
The command groups files by sub-repo prefix and commits atomically to each. Returns JSON: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`.
|
||||
|
||||
Record hashes from each repo in the response for SUMMARY tracking.
|
||||
|
||||
**If `sub_repos` is empty or not set:** Use standard git commit flow below.
|
||||
</sub_repos_commit_flow>
|
||||
|
||||
**5. Record hash:**
|
||||
```bash
|
||||
TASK_COMMIT=$(git rev-parse --short HEAD)
|
||||
@@ -370,7 +387,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not "
|
||||
|
||||
Include: duration, start/end times, task count, file count.
|
||||
|
||||
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition".
|
||||
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step".
|
||||
</step>
|
||||
|
||||
<step name="update_current_position">
|
||||
|
||||
105
get-shit-done/workflows/fast.md
Normal file
105
get-shit-done/workflows/fast.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<purpose>
|
||||
Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning,
|
||||
no research, no plan checking. Just: understand → do → commit → log.
|
||||
|
||||
For tasks like: fix a typo, update a config value, add a missing import, rename a
|
||||
variable, commit uncommitted work, add a .gitignore entry, bump a version number.
|
||||
|
||||
Use /gsd:quick for anything that needs multi-step planning or research.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="parse_task">
|
||||
Parse `$ARGUMENTS` for the task description.
|
||||
|
||||
If empty, ask:
|
||||
```
|
||||
What's the quick fix? (one sentence)
|
||||
```
|
||||
|
||||
Store as `$TASK`.
|
||||
</step>
|
||||
|
||||
<step name="scope_check">
|
||||
**Before doing anything, verify this is actually trivial.**
|
||||
|
||||
A task is trivial if it can be completed in:
|
||||
- ≤ 3 file edits
|
||||
- ≤ 1 minute of work
|
||||
- No new dependencies or architecture changes
|
||||
- No research needed
|
||||
|
||||
If the task seems non-trivial (multi-file refactor, new feature, needs research),
|
||||
say:
|
||||
|
||||
```
|
||||
This looks like it needs planning. Use /gsd:quick instead:
|
||||
/gsd:quick "{task description}"
|
||||
```
|
||||
|
||||
And stop.
|
||||
</step>
|
||||
|
||||
<step name="execute_inline">
|
||||
Do the work directly:
|
||||
|
||||
1. Read the relevant file(s)
|
||||
2. Make the change(s)
|
||||
3. Verify the change works (run existing tests if applicable, or do a quick sanity check)
|
||||
|
||||
**No PLAN.md.** Just do it.
|
||||
</step>
|
||||
|
||||
<step name="commit">
|
||||
Commit the change atomically:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: {concise description of what changed}"
|
||||
```
|
||||
|
||||
Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` as appropriate.
|
||||
</step>
|
||||
|
||||
<step name="log_to_state">
|
||||
If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table.
|
||||
If the table doesn't exist, skip this step silently.
|
||||
|
||||
```bash
|
||||
# Check if STATE.md has quick tasks table
|
||||
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
||||
# Append entry — workflow handles the format
|
||||
echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md
|
||||
fi
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="done">
|
||||
Report completion:
|
||||
|
||||
```
|
||||
✅ Done: {what was changed}
|
||||
Commit: {short hash}
|
||||
Files: {list of changed files}
|
||||
```
|
||||
|
||||
No next-step suggestions. No workflow routing. Just done.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<guardrails>
|
||||
- NEVER spawn a Task/subagent — this runs inline
|
||||
- NEVER create PLAN.md or SUMMARY.md files
|
||||
- NEVER run research or plan-checking
|
||||
- If the task takes more than 3 file edits, STOP and redirect to /gsd:quick
|
||||
- If you're unsure how to implement it, STOP and redirect to /gsd:quick
|
||||
</guardrails>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Task completed in current context (no subagents)
|
||||
- [ ] Atomic git commit with conventional message
|
||||
- [ ] STATE.md updated if it exists
|
||||
- [ ] Total operation under 2 minutes wall time
|
||||
</success_criteria>
|
||||
@@ -72,8 +72,8 @@ Errors: N | Warnings: N | Info: N
|
||||
```
|
||||
## Warnings
|
||||
|
||||
- [W001] STATE.md references phase 5, but only phases 1-3 exist
|
||||
Fix: Run /gsd:health --repair to regenerate
|
||||
- [W002] STATE.md references phase 5, but only phases 1-3 exist
|
||||
Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md
|
||||
|
||||
- [W005] Phase directory "1-setup" doesn't follow NN-name format
|
||||
Fix: Rename to match pattern (e.g., 01-setup)
|
||||
@@ -130,7 +130,7 @@ Report final status.
|
||||
| E004 | error | STATE.md not found | Yes |
|
||||
| E005 | error | config.json parse error | Yes |
|
||||
| W001 | warning | PROJECT.md missing required section | No |
|
||||
| W002 | warning | STATE.md references invalid phase | Yes |
|
||||
| W002 | warning | STATE.md references invalid phase | No |
|
||||
| W003 | warning | config.json not found | Yes |
|
||||
| W004 | warning | config.json invalid field value | No |
|
||||
| W005 | warning | Phase directory naming mismatch | No |
|
||||
@@ -148,7 +148,7 @@ Report final status.
|
||||
|--------|--------|------|
|
||||
| createConfig | Create config.json with defaults | None |
|
||||
| resetConfig | Delete + recreate config.json | Loses custom settings |
|
||||
| regenerateState | Create STATE.md from ROADMAP structure | Loses session history |
|
||||
| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history |
|
||||
| addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default |
|
||||
|
||||
**Not repairable (too risky):**
|
||||
@@ -157,3 +157,25 @@ Report final status.
|
||||
- Orphaned plan cleanup
|
||||
|
||||
</repair_actions>
|
||||
|
||||
<stale_task_cleanup>
|
||||
**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze.
|
||||
These are left behind when subagents are force-killed and consume disk space.
|
||||
|
||||
When `--repair` is active, detect and clean up:
|
||||
|
||||
```bash
|
||||
# Check for stale task directories (older than 24 hours)
|
||||
TASKS_DIR="$HOME/.claude/tasks"
|
||||
if [ -d "$TASKS_DIR" ]; then
|
||||
STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l)
|
||||
if [ "$STALE_COUNT" -gt 0 ]; then
|
||||
echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/"
|
||||
echo " These are leftover from crashed subagent sessions."
|
||||
echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)`
|
||||
</stale_task_cleanup>
|
||||
|
||||
@@ -151,6 +151,21 @@ Usage: `/gsd:quick`
|
||||
Usage: `/gsd:quick --research --full`
|
||||
Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:fast [description]`**
|
||||
Execute a trivial task inline — no subagents, no planning files, no overhead.
|
||||
|
||||
For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md.
|
||||
|
||||
- No PLAN.md or SUMMARY.md created
|
||||
- No subagent spawned (runs inline)
|
||||
- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial
|
||||
- Atomic commit with conventional message
|
||||
|
||||
Usage: `/gsd:fast "fix the typo in README"`
|
||||
Usage: `/gsd:fast "add .env to gitignore"`
|
||||
|
||||
### Roadmap Management
|
||||
|
||||
**`/gsd:add-phase <description>`**
|
||||
@@ -192,10 +207,12 @@ Start a new milestone through unified flow.
|
||||
- Optional domain research (spawns 4 parallel researcher agents)
|
||||
- Requirements definition with scoping
|
||||
- Roadmap creation with phase breakdown
|
||||
- Optional `--reset-phase-numbers` flag restarts numbering at Phase 1 and archives old phase dirs first for safety
|
||||
|
||||
Mirrors `/gsd:new-project` flow for brownfield projects (existing PROJECT.md).
|
||||
|
||||
Usage: `/gsd:new-milestone "v2.0 Features"`
|
||||
Usage: `/gsd:new-milestone --reset-phase-numbers "v2.0 Features"`
|
||||
|
||||
**`/gsd:complete-milestone <version>`**
|
||||
Archive completed milestone and prepare for next version.
|
||||
@@ -308,6 +325,65 @@ Validate built features through conversational UAT.
|
||||
|
||||
Usage: `/gsd:verify-work 3`
|
||||
|
||||
### Ship Work
|
||||
|
||||
**`/gsd:ship [phase]`**
|
||||
Create a PR from completed phase work with an auto-generated body.
|
||||
|
||||
- Pushes branch to remote
|
||||
- Creates PR with summary from SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md
|
||||
- Optionally requests code review
|
||||
- Updates STATE.md with shipping status
|
||||
|
||||
Prerequisites: Phase verified, `gh` CLI installed and authenticated.
|
||||
|
||||
Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`**
|
||||
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
|
||||
|
||||
- Detects available CLIs (gemini, claude, codex)
|
||||
- Each CLI reviews plans independently with the same structured prompt
|
||||
- Produces REVIEWS.md with per-reviewer feedback and consensus summary
|
||||
- Feed reviews back into planning: `/gsd:plan-phase N --reviews`
|
||||
|
||||
Usage: `/gsd:review --phase 3 --all`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:pr-branch [target]`**
|
||||
Create a clean branch for pull requests by filtering out .planning/ commits.
|
||||
|
||||
- Classifies commits: code-only (include), planning-only (exclude), mixed (include sans .planning/)
|
||||
- Cherry-picks code commits onto a clean branch
|
||||
- Reviewers see only code changes, no GSD artifacts
|
||||
|
||||
Usage: `/gsd:pr-branch` or `/gsd:pr-branch main`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:plant-seed [idea]`**
|
||||
Capture a forward-looking idea with trigger conditions for automatic surfacing.
|
||||
|
||||
- Seeds preserve WHY, WHEN to surface, and breadcrumbs to related code
|
||||
- Auto-surfaces during `/gsd:new-milestone` when trigger conditions match
|
||||
- Better than deferred items — triggers are checked, not forgotten
|
||||
|
||||
Usage: `/gsd:plant-seed "add real-time notifications when we build the events system"`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:audit-uat`**
|
||||
Cross-phase audit of all outstanding UAT and verification items.
|
||||
- Scans every phase for pending, skipped, blocked, and human_needed items
|
||||
- Cross-references against codebase to detect stale documentation
|
||||
- Produces prioritized human test plan grouped by testability
|
||||
- Use before starting a new milestone to clear verification debt
|
||||
|
||||
Usage: `/gsd:audit-uat`
|
||||
|
||||
### Milestone Auditing
|
||||
|
||||
**`/gsd:audit-milestone [version]`**
|
||||
|
||||
@@ -82,12 +82,25 @@ mkdir -p .planning/codebase
|
||||
Continue to spawn_agents.
|
||||
</step>
|
||||
|
||||
<step name="spawn_agents">
|
||||
<step name="detect_runtime_capabilities">
|
||||
Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation.
|
||||
|
||||
**Runtimes with Task tool:** Claude Code, Cursor (native subagent support)
|
||||
**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others
|
||||
|
||||
**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis):
|
||||
|
||||
→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead.
|
||||
|
||||
**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context.
|
||||
</step>
|
||||
|
||||
<step name="spawn_agents" condition="Task tool is available">
|
||||
Spawn 4 parallel gsd-codebase-mapper agents.
|
||||
|
||||
Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution.
|
||||
|
||||
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly.
|
||||
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly.
|
||||
|
||||
**Agent 1: Tech Focus**
|
||||
|
||||
@@ -172,9 +185,19 @@ Continue to collect_confirmations.
|
||||
</step>
|
||||
|
||||
<step name="collect_confirmations">
|
||||
Wait for all 4 agents to complete.
|
||||
Wait for all 4 agents to complete using TaskOutput tool.
|
||||
|
||||
Read each agent's output file to collect confirmations.
|
||||
**For each agent task_id returned by the Agent tool calls above:**
|
||||
```
|
||||
TaskOutput tool:
|
||||
task_id: "{task_id from Agent result}"
|
||||
block: true
|
||||
timeout: 300000
|
||||
```
|
||||
|
||||
Call TaskOutput for all 4 agents in parallel (single message with 4 TaskOutput calls).
|
||||
|
||||
Once all TaskOutput calls return, read each agent's output file to collect confirmations.
|
||||
|
||||
**Expected confirmation format from each agent:**
|
||||
```
|
||||
@@ -195,6 +218,37 @@ If any agent failed, note the failure and continue with successful documents.
|
||||
Continue to verify_output.
|
||||
</step>
|
||||
|
||||
<step name="sequential_mapping" condition="Task tool is NOT available (e.g. Antigravity, Gemini CLI, Codex)">
|
||||
When the `Task` tool is unavailable, perform codebase mapping sequentially in the current context. This replaces `spawn_agents` and `collect_confirmations`.
|
||||
|
||||
**IMPORTANT:** Do NOT use `browser_subagent`, `Explore`, or any browser-based tool. Use only file system tools (Read, Bash, Write, Grep, Glob, list_dir, view_file, grep_search, or equivalent tools available in your runtime).
|
||||
|
||||
Perform all 4 mapping passes sequentially:
|
||||
|
||||
**Pass 1: Tech Focus**
|
||||
- Explore package.json/Cargo.toml/go.mod/requirements.txt, config files, dependency trees
|
||||
- Write `.planning/codebase/STACK.md` — Languages, runtime, frameworks, dependencies, configuration
|
||||
- Write `.planning/codebase/INTEGRATIONS.md` — External APIs, databases, auth providers, webhooks
|
||||
|
||||
**Pass 2: Architecture Focus**
|
||||
- Explore directory structure, entry points, module boundaries, data flow
|
||||
- Write `.planning/codebase/ARCHITECTURE.md` — Pattern, layers, data flow, abstractions, entry points
|
||||
- Write `.planning/codebase/STRUCTURE.md` — Directory layout, key locations, naming conventions
|
||||
|
||||
**Pass 3: Quality Focus**
|
||||
- Explore code style, error handling patterns, test files, CI config
|
||||
- Write `.planning/codebase/CONVENTIONS.md` — Code style, naming, patterns, error handling
|
||||
- Write `.planning/codebase/TESTING.md` — Framework, structure, mocking, coverage
|
||||
|
||||
**Pass 4: Concerns Focus**
|
||||
- Explore TODOs, known issues, fragile areas, security patterns
|
||||
- Write `.planning/codebase/CONCERNS.md` — Tech debt, bugs, security, performance, fragile areas
|
||||
|
||||
Use the same document templates as the `gsd-codebase-mapper` agent. Include actual file paths formatted with backticks.
|
||||
|
||||
Continue to verify_output.
|
||||
</step>
|
||||
|
||||
<step name="verify_output">
|
||||
Verify all documents created successfully:
|
||||
|
||||
@@ -307,10 +361,10 @@ End workflow.
|
||||
|
||||
<success_criteria>
|
||||
- .planning/codebase/ directory created
|
||||
- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
|
||||
- Agents write documents directly (orchestrator doesn't receive document contents)
|
||||
- Read agent output files to collect confirmations
|
||||
- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
|
||||
- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent)
|
||||
- All 7 codebase documents exist
|
||||
- No empty documents (each should have >20 lines)
|
||||
- Clear completion summary with line counts
|
||||
- User offered clear next steps in GSD style
|
||||
</success_criteria>
|
||||
|
||||
@@ -14,6 +14,12 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
|
||||
## 1. Load Context
|
||||
|
||||
Parse `$ARGUMENTS` before doing anything else:
|
||||
- `--reset-phase-numbers` flag → opt into restarting roadmap phase numbering at `1`
|
||||
- remaining text → use as milestone name if present
|
||||
|
||||
If the flag is absent, keep the current behavior of continuing phase numbering from the previous milestone.
|
||||
|
||||
- Read PROJECT.md (existing project, validated requirements, decisions)
|
||||
- Read MILESTONES.md (what shipped previously)
|
||||
- Read STATE.md (pending todos, blockers)
|
||||
@@ -54,6 +60,27 @@ Add/update:
|
||||
|
||||
Update Active requirements section and "Last updated" footer.
|
||||
|
||||
Ensure the `## Evolution` section exists in PROJECT.md. If missing (projects created before this feature), add it before the footer:
|
||||
|
||||
```markdown
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
**After each phase transition** (via `/gsd:transition`):
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
2. Requirements validated? → Move to Validated with phase reference
|
||||
3. New requirements emerged? → Add to Active
|
||||
4. Decisions to log? → Add to Key Decisions
|
||||
5. "What This Is" still accurate? → Update if drifted
|
||||
|
||||
**After each milestone** (via `/gsd:complete-milestone`):
|
||||
1. Full review of all sections
|
||||
2. Core Value check — still the right priority?
|
||||
3. Audit Out of Scope — reasons still valid?
|
||||
4. Update Context with current state
|
||||
```
|
||||
|
||||
## 5. Update STATE.md
|
||||
|
||||
```markdown
|
||||
@@ -82,7 +109,27 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`.
|
||||
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`.
|
||||
|
||||
## 7.5 Reset-phase safety (only when `--reset-phase-numbers`)
|
||||
|
||||
If `--reset-phase-numbers` is active:
|
||||
|
||||
1. Set starting phase number to `1` for the upcoming roadmap.
|
||||
2. If `phase_dir_count > 0`, archive the old phase directories before roadmapping so new `01-*` / `02-*` directories cannot collide with stale milestone directories.
|
||||
|
||||
If `phase_dir_count > 0` and `phase_archive_path` is available:
|
||||
|
||||
```bash
|
||||
mkdir -p "${phase_archive_path}"
|
||||
find .planning/phases -mindepth 1 -maxdepth 1 -type d -exec mv {} "${phase_archive_path}/" \;
|
||||
```
|
||||
|
||||
Then verify `.planning/phases/` no longer contains old milestone directories before continuing.
|
||||
|
||||
If `phase_dir_count > 0` but `phase_archive_path` is missing:
|
||||
- Stop and explain that reset numbering is unsafe without a completed milestone archive target.
|
||||
- Tell the user to complete/archive the previous milestone first, then rerun `/gsd:new-milestone --reset-phase-numbers`.
|
||||
|
||||
## 8. Research Decision
|
||||
|
||||
@@ -270,7 +317,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milest
|
||||
◆ Spawning roadmapper...
|
||||
```
|
||||
|
||||
**Starting phase number:** Read MILESTONES.md for last phase number. Continue from there (v1.0 ended at phase 5 → v1.1 starts at phase 6).
|
||||
**Starting phase number:**
|
||||
- If `--reset-phase-numbers` is active, start at **Phase 1**
|
||||
- Otherwise, continue from the previous milestone's last phase number (v1.0 ended at phase 5 → v1.1 starts at phase 6)
|
||||
|
||||
```
|
||||
Task(prompt="
|
||||
@@ -286,7 +335,9 @@ Task(prompt="
|
||||
|
||||
<instructions>
|
||||
Create roadmap for milestone v[X.Y]:
|
||||
1. Start phase numbering from [N]
|
||||
1. Respect the selected numbering mode:
|
||||
- `--reset-phase-numbers` → start at Phase 1
|
||||
- default behavior → continue from the previous milestone's last phase number
|
||||
2. Derive phases from THIS MILESTONE's requirements only
|
||||
3. Map every requirement to exactly one phase
|
||||
4. Derive 2-5 success criteria per phase (observable user behaviors)
|
||||
@@ -378,7 +429,7 @@ Also: `/gsd:plan-phase [N]` — skip discussion, plan directly
|
||||
- [ ] gsd-roadmapper spawned with phase numbering context
|
||||
- [ ] Roadmap files written immediately (not draft)
|
||||
- [ ] User feedback incorporated (if any)
|
||||
- [ ] ROADMAP.md phases continue from previous milestone
|
||||
- [ ] Phase numbering mode respected (continued or reset)
|
||||
- [ ] All commits made (if planning docs committed)
|
||||
- [ ] User knows next step: `/gsd:discuss-phase [N]`
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
</required_reading>
|
||||
|
||||
<auto_mode>
|
||||
|
||||
## Auto Mode Detection
|
||||
|
||||
Check if `--auto` flag is present in $ARGUMENTS.
|
||||
|
||||
**If auto mode:**
|
||||
|
||||
- Skip brownfield mapping offer (assume greenfield)
|
||||
- Skip deep questioning (extract context from provided document)
|
||||
- Config: YOLO mode is implicit (skip that question), but ask granularity/git/agents FIRST (Step 2a)
|
||||
@@ -23,6 +25,7 @@ Check if `--auto` flag is present in $ARGUMENTS.
|
||||
|
||||
**Document requirement:**
|
||||
Auto mode requires an idea document — either:
|
||||
|
||||
- File reference: `/gsd:new-project --auto @prd.md`
|
||||
- Pasted/written text in the prompt
|
||||
|
||||
@@ -37,6 +40,7 @@ Usage:
|
||||
|
||||
The document should describe what you want to build.
|
||||
```
|
||||
|
||||
</auto_mode>
|
||||
|
||||
<process>
|
||||
@@ -55,6 +59,7 @@ Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `co
|
||||
**If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`.
|
||||
|
||||
**If `has_git` is false:** Initialize git:
|
||||
|
||||
```bash
|
||||
git init
|
||||
```
|
||||
@@ -66,6 +71,7 @@ git init
|
||||
**If `needs_codebase_map` is true** (from init — existing code detected but no codebase map):
|
||||
|
||||
Use AskUserQuestion:
|
||||
|
||||
- header: "Codebase"
|
||||
- question: "I detected existing code in this directory. Would you like to map the codebase first?"
|
||||
- options:
|
||||
@@ -73,9 +79,11 @@ Use AskUserQuestion:
|
||||
- "Skip mapping" — Proceed with project initialization
|
||||
|
||||
**If "Map codebase first":**
|
||||
|
||||
```
|
||||
Run `/gsd:map-codebase` first, then return to `/gsd:new-project`
|
||||
```
|
||||
|
||||
Exit command.
|
||||
|
||||
**If "Skip mapping" OR `needs_codebase_map` is false:** Continue to Step 3.
|
||||
@@ -210,11 +218,20 @@ Ask inline (freeform, NOT AskUserQuestion):
|
||||
|
||||
Wait for their response. This gives you the context needed to ask intelligent follow-up questions.
|
||||
|
||||
**Research-before-questions mode:** Check if `research_questions` is enabled in `.planning/config.json` (or the config from init context). When enabled, before asking follow-up questions about a topic area:
|
||||
|
||||
1. Do a brief web search for best practices related to what the user described
|
||||
2. Mention key findings naturally as you ask questions (e.g., "Most projects like this use X — is that what you're thinking, or something different?")
|
||||
3. This makes questions more informed without changing the conversational flow
|
||||
|
||||
When disabled (default), ask questions directly as before.
|
||||
|
||||
**Follow the thread:**
|
||||
|
||||
Based on what they said, ask follow-up questions that dig into their response. Use AskUserQuestion with options that probe what they mentioned — interpretations, clarifications, concrete examples.
|
||||
|
||||
Keep following threads. Each answer opens new threads to explore. Ask about:
|
||||
|
||||
- What excited them
|
||||
- What problem sparked this
|
||||
- What they mean by vague terms
|
||||
@@ -222,6 +239,7 @@ Keep following threads. Each answer opens new threads to explore. Ask about:
|
||||
- What's already decided
|
||||
|
||||
Consult `questioning.md` for techniques:
|
||||
|
||||
- Challenge vagueness
|
||||
- Make abstract concrete
|
||||
- Surface assumptions
|
||||
@@ -323,6 +341,27 @@ Initialize with any decisions made during questioning:
|
||||
*Last updated: [date] after initialization*
|
||||
```
|
||||
|
||||
**Evolution section** (include at the end of PROJECT.md, before the footer):
|
||||
|
||||
```markdown
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
**After each phase transition** (via `/gsd:transition`):
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
2. Requirements validated? → Move to Validated with phase reference
|
||||
3. New requirements emerged? → Add to Active
|
||||
4. Decisions to log? → Add to Key Decisions
|
||||
5. "What This Is" still accurate? → Update if drifted
|
||||
|
||||
**After each milestone** (via `/gsd:complete-milestone`):
|
||||
1. Full review of all sections
|
||||
2. Core Value check — still the right priority?
|
||||
3. Audit Out of Scope — reasons still valid?
|
||||
4. Update Context with current state
|
||||
```
|
||||
|
||||
Do not compress. Capture everything gathered.
|
||||
|
||||
**Commit PROJECT.md:**
|
||||
@@ -465,10 +504,12 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode"
|
||||
**Note:** Run `/gsd:settings` anytime to update model profile, workflow agents, branching strategy, and other preferences.
|
||||
|
||||
**If commit_docs = No:**
|
||||
|
||||
- Set `commit_docs: false` in config.json
|
||||
- Add `.planning/` to `.gitignore` (create if needed)
|
||||
|
||||
**If commit_docs = Yes:**
|
||||
|
||||
- No additional gitignore entries needed
|
||||
|
||||
**Commit config.json:**
|
||||
@@ -477,6 +518,38 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project '{"mode"
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project config" --files .planning/config.json
|
||||
```
|
||||
|
||||
## 5.1. Sub-Repo Detection
|
||||
|
||||
**Detect multi-repo workspace:**
|
||||
|
||||
Check for directories with their own `.git` folders (separate repos within the workspace):
|
||||
|
||||
```bash
|
||||
find . -maxdepth 1 -type d -not -name ".*" -not -name "node_modules" -exec test -d "{}/.git" \; -print
|
||||
```
|
||||
|
||||
**If sub-repos found:**
|
||||
|
||||
Strip the `./` prefix to get directory names (e.g., `./backend` → `backend`).
|
||||
|
||||
Use AskUserQuestion:
|
||||
|
||||
- header: "Multi-Repo Workspace"
|
||||
- question: "I detected separate git repos in this workspace. Which directories contain code that GSD should commit to?"
|
||||
- multiSelect: true
|
||||
- options: one option per detected directory
|
||||
- "[directory name]" — Separate git repo
|
||||
|
||||
**If user selects one or more directories:**
|
||||
|
||||
- Set `planning.sub_repos` in config.json to the selected directory names array (e.g., `["backend", "frontend"]`)
|
||||
- Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces)
|
||||
- Add `.planning/` to `.gitignore` if not already present
|
||||
|
||||
Config changes are saved locally — no commit needed since `commit_docs` is `false` in multi-repo mode.
|
||||
|
||||
**If no sub-repos found or user selects none:** Continue with no changes to config.
|
||||
|
||||
## 5.5. Resolve Model Profile
|
||||
|
||||
Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model`.
|
||||
@@ -486,6 +559,7 @@ Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model
|
||||
**If auto mode:** Default to "Research first" without asking.
|
||||
|
||||
Use AskUserQuestion:
|
||||
|
||||
- header: "Research"
|
||||
- question: "Research the domain ecosystem before defining requirements?"
|
||||
- options:
|
||||
@@ -495,6 +569,7 @@ Use AskUserQuestion:
|
||||
**If "Research first":**
|
||||
|
||||
Display stage banner:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► RESEARCHING
|
||||
@@ -504,6 +579,7 @@ Researching [domain] ecosystem...
|
||||
```
|
||||
|
||||
Create research directory:
|
||||
|
||||
```bash
|
||||
mkdir -p .planning/research
|
||||
```
|
||||
@@ -511,10 +587,12 @@ mkdir -p .planning/research
|
||||
**Determine milestone context:**
|
||||
|
||||
Check if this is greenfield or subsequent milestone:
|
||||
|
||||
- If no "Validated" requirements in PROJECT.md → Greenfield (building from scratch)
|
||||
- If "Validated" requirements exist → Subsequent milestone (adding to existing app)
|
||||
|
||||
Display spawning indicator:
|
||||
|
||||
```
|
||||
◆ Spawning 4 researchers in parallel...
|
||||
→ Stack research
|
||||
@@ -703,6 +781,7 @@ Commit after writing.
|
||||
```
|
||||
|
||||
Display research complete banner and key findings:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► RESEARCH COMPLETE ✓
|
||||
@@ -722,6 +801,7 @@ Files: `.planning/research/`
|
||||
## 7. Define Requirements
|
||||
|
||||
Display stage banner:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► DEFINING REQUIREMENTS
|
||||
@@ -731,6 +811,7 @@ Display stage banner:
|
||||
**Load context:**
|
||||
|
||||
Read PROJECT.md and extract:
|
||||
|
||||
- Core value (the ONE thing that must work)
|
||||
- Stated constraints (budget, timeline, tech limitations)
|
||||
- Any explicit scope boundaries
|
||||
@@ -738,6 +819,7 @@ Read PROJECT.md and extract:
|
||||
**If research exists:** Read research/FEATURES.md and extract feature categories.
|
||||
|
||||
**If auto mode:**
|
||||
|
||||
- Auto-include all table stakes features (users expect these)
|
||||
- Include features explicitly mentioned in provided document
|
||||
- Auto-defer differentiators not mentioned in document
|
||||
@@ -776,6 +858,7 @@ Here are the features for [domain]:
|
||||
Ask: "What are the main things users need to be able to do?"
|
||||
|
||||
For each capability mentioned:
|
||||
|
||||
- Ask clarifying questions to make it specific
|
||||
- Probe for related capabilities
|
||||
- Group into categories
|
||||
@@ -794,6 +877,7 @@ For each category, use AskUserQuestion:
|
||||
- "None for v1" — Defer entire category
|
||||
|
||||
Track responses:
|
||||
|
||||
- Selected features → v1 requirements
|
||||
- Unselected table stakes → v2 (users expect these)
|
||||
- Unselected differentiators → out of scope
|
||||
@@ -801,6 +885,7 @@ Track responses:
|
||||
**Identify gaps:**
|
||||
|
||||
Use AskUserQuestion:
|
||||
|
||||
- header: "Additions"
|
||||
- question: "Any requirements research missed? (Features specific to your vision)"
|
||||
- options:
|
||||
@@ -814,6 +899,7 @@ Cross-check requirements against Core Value from PROJECT.md. If gaps detected, s
|
||||
**Generate REQUIREMENTS.md:**
|
||||
|
||||
Create `.planning/REQUIREMENTS.md` with:
|
||||
|
||||
- v1 Requirements grouped by category (checkboxes, REQ-IDs)
|
||||
- v2 Requirements (deferred)
|
||||
- Out of Scope (explicit exclusions with reasoning)
|
||||
@@ -824,12 +910,14 @@ Create `.planning/REQUIREMENTS.md` with:
|
||||
**Requirement quality criteria:**
|
||||
|
||||
Good requirements are:
|
||||
|
||||
- **Specific and testable:** "User can reset password via email link" (not "Handle password reset")
|
||||
- **User-centric:** "User can X" (not "System does Y")
|
||||
- **Atomic:** One capability per requirement (not "User can login and manage profile")
|
||||
- **Independent:** Minimal dependencies on other requirements
|
||||
|
||||
Reject vague requirements. Push for specificity:
|
||||
|
||||
- "Handle authentication" → "User can log in with email/password and stay logged in across sessions"
|
||||
- "Support sharing" → "User can share post via link that opens in recipient's browser"
|
||||
|
||||
@@ -867,6 +955,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define v1 req
|
||||
## 8. Create Roadmap
|
||||
|
||||
Display stage banner:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► CREATING ROADMAP
|
||||
@@ -907,6 +996,7 @@ Write files first, then return. This ensures artifacts persist even if context i
|
||||
**Handle roadmapper return:**
|
||||
|
||||
**If `## ROADMAP BLOCKED`:**
|
||||
|
||||
- Present blocker information
|
||||
- Work with user to resolve
|
||||
- Re-spawn when resolved
|
||||
@@ -956,6 +1046,7 @@ Success criteria:
|
||||
**CRITICAL: Ask for approval before committing (interactive mode only):**
|
||||
|
||||
Use AskUserQuestion:
|
||||
|
||||
- header: "Roadmap"
|
||||
- question: "Does this roadmap structure work for you?"
|
||||
- options:
|
||||
@@ -966,8 +1057,10 @@ Use AskUserQuestion:
|
||||
**If "Approve":** Continue to commit.
|
||||
|
||||
**If "Adjust phases":**
|
||||
|
||||
- Get user's adjustment notes
|
||||
- Re-spawn roadmapper with revision context:
|
||||
|
||||
```
|
||||
Task(prompt="
|
||||
<revision>
|
||||
@@ -983,15 +1076,24 @@ Use AskUserQuestion:
|
||||
</revision>
|
||||
", subagent_type="gsd-roadmapper", model="{roadmapper_model}", description="Revise roadmap")
|
||||
```
|
||||
|
||||
- Present revised roadmap
|
||||
- Loop until user approves
|
||||
|
||||
**If "Review full file":** Display raw `cat .planning/ROADMAP.md`, then re-ask.
|
||||
|
||||
**Generate or refresh project CLAUDE.md before final commit:**
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-claude-md
|
||||
```
|
||||
|
||||
This ensures new projects get the default GSD workflow-enforcement guidance and current project context in `CLAUDE.md`.
|
||||
|
||||
**Commit roadmap (after approval or auto mode):**
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
## 9. Done
|
||||
@@ -1012,6 +1114,7 @@ Present completion summary:
|
||||
| Research | `.planning/research/` |
|
||||
| Requirements | `.planning/REQUIREMENTS.md` |
|
||||
| Roadmap | `.planning/ROADMAP.md` |
|
||||
| Project guide | `CLAUDE.md` |
|
||||
|
||||
**[N] phases** | **[X] requirements** | Ready to build ✓
|
||||
```
|
||||
@@ -1062,6 +1165,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto")
|
||||
- `.planning/REQUIREMENTS.md`
|
||||
- `.planning/ROADMAP.md`
|
||||
- `.planning/STATE.md`
|
||||
- `CLAUDE.md`
|
||||
|
||||
</output>
|
||||
|
||||
@@ -1083,6 +1187,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto")
|
||||
- [ ] ROADMAP.md created with phases, requirement mappings, success criteria
|
||||
- [ ] STATE.md initialized
|
||||
- [ ] REQUIREMENTS.md traceability updated
|
||||
- [ ] CLAUDE.md generated with GSD workflow guidance
|
||||
- [ ] User knows next step is `/gsd:discuss-phase 1`
|
||||
|
||||
**Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist.
|
||||
|
||||
97
get-shit-done/workflows/next.md
Normal file
97
get-shit-done/workflows/next.md
Normal file
@@ -0,0 +1,97 @@
|
||||
<purpose>
|
||||
Detect current project state and automatically advance to the next logical GSD workflow step.
|
||||
Reads project state to determine: discuss → plan → execute → verify → complete progression.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="detect_state">
|
||||
Read project state to determine current position:
|
||||
|
||||
```bash
|
||||
# Get state snapshot
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state json 2>/dev/null || echo "{}"
|
||||
```
|
||||
|
||||
Also read:
|
||||
- `.planning/STATE.md` — current phase, progress, plan counts
|
||||
- `.planning/ROADMAP.md` — milestone structure and phase list
|
||||
|
||||
Extract:
|
||||
- `current_phase` — which phase is active
|
||||
- `plan_of` / `plans_total` — plan execution progress
|
||||
- `progress` — overall percentage
|
||||
- `status` — active, paused, etc.
|
||||
|
||||
If no `.planning/` directory exists:
|
||||
```
|
||||
No GSD project detected. Run `/gsd:new-project` to get started.
|
||||
```
|
||||
Exit.
|
||||
</step>
|
||||
|
||||
<step name="determine_next_action">
|
||||
Apply routing rules based on state:
|
||||
|
||||
**Route 1: No phases exist yet → discuss**
|
||||
If ROADMAP has phases but no phase directories exist on disk:
|
||||
→ Next action: `/gsd:discuss-phase <first-phase>`
|
||||
|
||||
**Route 2: Phase exists but has no CONTEXT.md or RESEARCH.md → discuss**
|
||||
If the current phase directory exists but has neither CONTEXT.md nor RESEARCH.md:
|
||||
→ Next action: `/gsd:discuss-phase <current-phase>`
|
||||
|
||||
**Route 3: Phase has context but no plans → plan**
|
||||
If the current phase has CONTEXT.md (or RESEARCH.md) but no PLAN.md files:
|
||||
→ Next action: `/gsd:plan-phase <current-phase>`
|
||||
|
||||
**Route 4: Phase has plans but incomplete summaries → execute**
|
||||
If plans exist but not all have matching summaries:
|
||||
→ Next action: `/gsd:execute-phase <current-phase>`
|
||||
|
||||
**Route 5: All plans have summaries → verify and complete**
|
||||
If all plans in the current phase have summaries:
|
||||
→ Next action: `/gsd:verify-work` then `/gsd:complete-phase`
|
||||
|
||||
**Route 6: Phase complete, next phase exists → advance**
|
||||
If the current phase is complete and the next phase exists in ROADMAP:
|
||||
→ Next action: `/gsd:discuss-phase <next-phase>`
|
||||
|
||||
**Route 7: All phases complete → complete milestone**
|
||||
If all phases are complete:
|
||||
→ Next action: `/gsd:complete-milestone`
|
||||
|
||||
**Route 8: Paused → resume**
|
||||
If STATE.md shows paused_at:
|
||||
→ Next action: `/gsd:resume-work`
|
||||
</step>
|
||||
|
||||
<step name="show_and_execute">
|
||||
Display the determination:
|
||||
|
||||
```
|
||||
## GSD Next
|
||||
|
||||
**Current:** Phase [N] — [name] | [progress]%
|
||||
**Status:** [status description]
|
||||
|
||||
▶ **Next step:** `/gsd:[command] [args]`
|
||||
[One-line explanation of why this is the next step]
|
||||
```
|
||||
|
||||
Then immediately invoke the determined command via SlashCommand.
|
||||
Do not ask for confirmation — the whole point of `/gsd:next` is zero-friction advancement.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Project state correctly detected
|
||||
- [ ] Next action correctly determined from routing rules
|
||||
- [ ] Command invoked immediately without user confirmation
|
||||
- [ ] Clear status shown before invoking
|
||||
</success_criteria>
|
||||
@@ -1,5 +1,5 @@
|
||||
<purpose>
|
||||
Create `.continue-here.md` handoff file to preserve complete work state across sessions. Enables seamless resumption with full context restoration.
|
||||
Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -27,10 +27,61 @@ If no active phase detected, ask user which phase they're pausing work on.
|
||||
3. **Work remaining**: What's left in current plan/phase
|
||||
4. **Decisions made**: Key decisions and rationale
|
||||
5. **Blockers/issues**: Anything stuck
|
||||
6. **Mental context**: The approach, next steps, "vibe"
|
||||
7. **Files modified**: What's changed but not committed
|
||||
6. **Human actions pending**: Things that need manual intervention (MCP setup, API keys, approvals, manual testing)
|
||||
7. **Background processes**: Any running servers/watchers that were part of the workflow
|
||||
8. **Files modified**: What's changed but not committed
|
||||
|
||||
Ask user for clarifications if needed via conversational questions.
|
||||
|
||||
**Also inspect SUMMARY.md files for false completions:**
|
||||
```bash
|
||||
# Check for placeholder content in existing summaries
|
||||
grep -l "To be filled\|placeholder\|TBD" .planning/phases/*/*.md 2>/dev/null
|
||||
```
|
||||
Report any summaries with placeholder content as incomplete items.
|
||||
</step>
|
||||
|
||||
<step name="write_structured">
|
||||
**Write structured handoff to `.planning/HANDOFF.json`:**
|
||||
|
||||
```bash
|
||||
timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timestamp full --raw)
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"timestamp": "{timestamp}",
|
||||
"phase": "{phase_number}",
|
||||
"phase_name": "{phase_name}",
|
||||
"phase_dir": "{phase_dir}",
|
||||
"plan": {current_plan_number},
|
||||
"task": {current_task_number},
|
||||
"total_tasks": {total_task_count},
|
||||
"status": "paused",
|
||||
"completed_tasks": [
|
||||
{"id": 1, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
|
||||
{"id": 2, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
|
||||
{"id": 3, "name": "{task_name}", "status": "in_progress", "progress": "{what_done}"}
|
||||
],
|
||||
"remaining_tasks": [
|
||||
{"id": 4, "name": "{task_name}", "status": "not_started"},
|
||||
{"id": 5, "name": "{task_name}", "status": "not_started"}
|
||||
],
|
||||
"blockers": [
|
||||
{"description": "{blocker}", "type": "technical|human_action|external", "workaround": "{if any}"}
|
||||
],
|
||||
"human_actions_pending": [
|
||||
{"action": "{what needs to be done}", "context": "{why}", "blocking": true}
|
||||
],
|
||||
"decisions": [
|
||||
{"decision": "{what}", "rationale": "{why}", "phase": "{phase_number}"}
|
||||
],
|
||||
"uncommitted_files": [],
|
||||
"next_action": "{specific first action when resuming}",
|
||||
"context_notes": "{mental state, approach, what you were thinking}"
|
||||
}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="write">
|
||||
@@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta
|
||||
|
||||
<step name="commit">
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md .planning/HANDOFF.json
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="confirm">
|
||||
```
|
||||
✓ Handoff created: .planning/phases/[XX-name]/.continue-here.md
|
||||
✓ Handoff created:
|
||||
- .planning/HANDOFF.json (structured, machine-readable)
|
||||
- .planning/phases/[XX-name]/.continue-here.md (human-readable)
|
||||
|
||||
Current state:
|
||||
|
||||
- Phase: [XX-name]
|
||||
- Task: [X] of [Y]
|
||||
- Status: [in_progress/blocked]
|
||||
- Blockers: [count] ({human_actions_pending count} need human action)
|
||||
- Committed as WIP
|
||||
|
||||
To resume: /gsd:resume-work
|
||||
|
||||
@@ -8,6 +8,13 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
@~/.claude/get-shit-done/references/ui-brand.md
|
||||
</required_reading>
|
||||
|
||||
<available_agent_types>
|
||||
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
|
||||
- gsd-phase-researcher — Researches technical approaches for a phase
|
||||
- gsd-planner — Creates detailed plans from phase scope
|
||||
- gsd-plan-checker — Reviews plan quality before execution
|
||||
</available_agent_types>
|
||||
|
||||
<process>
|
||||
|
||||
## 1. Initialize
|
||||
@@ -170,7 +177,16 @@ Use AskUserQuestion:
|
||||
- "Run discuss-phase first" — Capture design decisions before planning
|
||||
|
||||
If "Continue without context": Proceed to step 5.
|
||||
If "Run discuss-phase first": Display `/gsd:discuss-phase {X}` and exit workflow.
|
||||
If "Run discuss-phase first":
|
||||
**IMPORTANT:** Do NOT invoke discuss-phase as a nested Skill/Task call — AskUserQuestion
|
||||
does not work correctly in nested subcontexts (#1009). Instead, display the command
|
||||
and exit so the user runs it as a top-level command:
|
||||
```
|
||||
Run this command first, then re-run /gsd:plan-phase {X}:
|
||||
|
||||
/gsd:discuss-phase {X}
|
||||
```
|
||||
**Exit the plan-phase workflow. Do not continue.**
|
||||
|
||||
## 5. Handle Research
|
||||
|
||||
@@ -571,11 +587,62 @@ Display: `Max iterations reached. {N} issues remain:` + issue list
|
||||
|
||||
Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
|
||||
|
||||
## 13. Present Final Status
|
||||
## 13. Requirements Coverage Gate
|
||||
|
||||
After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.
|
||||
|
||||
**Skip if:** `phase_req_ids` is null or TBD (no requirements mapped to this phase).
|
||||
|
||||
**Step 1: Extract requirement IDs claimed by plans**
|
||||
```bash
|
||||
# Collect all requirement IDs from plan frontmatter
|
||||
PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u)
|
||||
```
|
||||
|
||||
**Step 2: Compare against phase requirements from ROADMAP**
|
||||
|
||||
For each REQ-ID in `phase_req_ids`:
|
||||
- If REQ-ID appears in `PLAN_REQS` → covered ✓
|
||||
- If REQ-ID does NOT appear in any plan → uncovered ✗
|
||||
|
||||
**Step 3: Check CONTEXT.md features against plan objectives**
|
||||
|
||||
Read CONTEXT.md `<decisions>` section. Extract feature/capability names. Check each against plan `<objective>` blocks. Features not mentioned in any plan objective → potentially dropped.
|
||||
|
||||
**Step 4: Report**
|
||||
|
||||
If all requirements covered and no dropped features:
|
||||
```
|
||||
✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans
|
||||
```
|
||||
→ Proceed to step 14.
|
||||
|
||||
If gaps found:
|
||||
```
|
||||
## ⚠ Requirements Coverage Gap
|
||||
|
||||
{M} of {N} phase requirements are not assigned to any plan:
|
||||
|
||||
| REQ-ID | Description | Plans |
|
||||
|--------|-------------|-------|
|
||||
| {id} | {from REQUIREMENTS.md} | None |
|
||||
|
||||
{K} CONTEXT.md features not found in plan objectives:
|
||||
- {feature_name} — described in CONTEXT.md but no plan covers it
|
||||
|
||||
Options:
|
||||
1. Re-plan to include missing requirements (recommended)
|
||||
2. Move uncovered requirements to next phase
|
||||
3. Proceed anyway — accept coverage gaps
|
||||
```
|
||||
|
||||
Use AskUserQuestion to present the options.
|
||||
|
||||
## 14. Present Final Status
|
||||
|
||||
Route to `<offer_next>` OR `auto_advance` depending on flags/config.
|
||||
|
||||
## 14. Auto-Advance Check
|
||||
## 15. Auto-Advance Check
|
||||
|
||||
Check for auto-advance trigger:
|
||||
|
||||
@@ -670,6 +737,30 @@ Verification: {Passed | Passed with override | Skipped}
|
||||
───────────────────────────────────────────────────────────────
|
||||
</offer_next>
|
||||
|
||||
<windows_troubleshooting>
|
||||
**Windows users:** If plan-phase freezes during agent spawning (common on Windows due to
|
||||
stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126):
|
||||
|
||||
1. **Force-kill:** Close the terminal (Ctrl+C may not work)
|
||||
2. **Clean up orphaned processes:**
|
||||
```powershell
|
||||
# Kill orphaned node processes from stale MCP servers
|
||||
Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force
|
||||
```
|
||||
3. **Clean up stale task directories:**
|
||||
```powershell
|
||||
# Remove stale subagent task dirs (Claude Code never cleans these on crash)
|
||||
Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue
|
||||
```
|
||||
4. **Reduce MCP server count:** Temporarily disable non-essential MCP servers in settings.json
|
||||
5. **Retry:** Restart Claude Code and run `/gsd:plan-phase` again
|
||||
|
||||
If freezes persist, try `--skip-research` to reduce the agent chain from 3 to 2 agents:
|
||||
```
|
||||
/gsd:plan-phase N --skip-research
|
||||
```
|
||||
</windows_troubleshooting>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] .planning/ directory validated
|
||||
- [ ] Phase validated against roadmap
|
||||
|
||||
169
get-shit-done/workflows/plant-seed.md
Normal file
169
get-shit-done/workflows/plant-seed.md
Normal file
@@ -0,0 +1,169 @@
|
||||
<purpose>
|
||||
Capture a forward-looking idea as a structured seed file with trigger conditions.
|
||||
Seeds auto-surface during /gsd:new-milestone when trigger conditions match the
|
||||
new milestone's scope.
|
||||
|
||||
Seeds beat deferred items because they:
|
||||
- Preserve WHY the idea matters (not just WHAT)
|
||||
- Define WHEN to surface (trigger conditions, not manual scanning)
|
||||
- Track breadcrumbs (code references, related decisions)
|
||||
- Auto-present at the right time via new-milestone scan
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="parse_idea">
|
||||
Parse `$ARGUMENTS` for the idea summary.
|
||||
|
||||
If empty, ask:
|
||||
```
|
||||
What's the idea? (one sentence)
|
||||
```
|
||||
|
||||
Store as `$IDEA`.
|
||||
</step>
|
||||
|
||||
<step name="create_seed_dir">
|
||||
```bash
|
||||
mkdir -p .planning/seeds
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="gather_context">
|
||||
Ask focused questions to build a complete seed:
|
||||
|
||||
```
|
||||
AskUserQuestion(
|
||||
header: "Trigger",
|
||||
question: "When should this idea surface? (e.g., 'when we add user accounts', 'next major version', 'when performance becomes a priority')",
|
||||
options: [] // freeform
|
||||
)
|
||||
```
|
||||
|
||||
Store as `$TRIGGER`.
|
||||
|
||||
```
|
||||
AskUserQuestion(
|
||||
header: "Why",
|
||||
question: "Why does this matter? What problem does it solve or what opportunity does it create?",
|
||||
options: []
|
||||
)
|
||||
```
|
||||
|
||||
Store as `$WHY`.
|
||||
|
||||
```
|
||||
AskUserQuestion(
|
||||
header: "Scope",
|
||||
question: "How big is this? (rough estimate)",
|
||||
options: [
|
||||
{ label: "Small", description: "A few hours — could be a quick task" },
|
||||
{ label: "Medium", description: "A phase or two — needs planning" },
|
||||
{ label: "Large", description: "A full milestone — significant effort" }
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Store as `$SCOPE`.
|
||||
</step>
|
||||
|
||||
<step name="collect_breadcrumbs">
|
||||
Search the codebase for relevant references:
|
||||
|
||||
```bash
|
||||
# Find files related to the idea keywords
|
||||
grep -rl "$KEYWORD" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null | head -10
|
||||
```
|
||||
|
||||
Also check:
|
||||
- Current STATE.md for related decisions
|
||||
- ROADMAP.md for related phases
|
||||
- todos/ for related captured ideas
|
||||
|
||||
Store relevant file paths as `$BREADCRUMBS`.
|
||||
</step>
|
||||
|
||||
<step name="generate_seed_id">
|
||||
```bash
|
||||
# Find next seed number
|
||||
EXISTING=$(ls .planning/seeds/SEED-*.md 2>/dev/null | wc -l)
|
||||
NEXT=$((EXISTING + 1))
|
||||
PADDED=$(printf "%03d" $NEXT)
|
||||
```
|
||||
|
||||
Generate slug from idea summary.
|
||||
</step>
|
||||
|
||||
<step name="write_seed">
|
||||
Write `.planning/seeds/SEED-{PADDED}-{slug}.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: SEED-{PADDED}
|
||||
status: dormant
|
||||
planted: {ISO date}
|
||||
planted_during: {current milestone/phase from STATE.md}
|
||||
trigger_when: {$TRIGGER}
|
||||
scope: {$SCOPE}
|
||||
---
|
||||
|
||||
# SEED-{PADDED}: {$IDEA}
|
||||
|
||||
## Why This Matters
|
||||
|
||||
{$WHY}
|
||||
|
||||
## When to Surface
|
||||
|
||||
**Trigger:** {$TRIGGER}
|
||||
|
||||
This seed should be presented during `/gsd:new-milestone` when the milestone
|
||||
scope matches any of these conditions:
|
||||
- {trigger condition 1}
|
||||
- {trigger condition 2}
|
||||
|
||||
## Scope Estimate
|
||||
|
||||
**{$SCOPE}** — {elaboration based on scope choice}
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
Related code and decisions found in the current codebase:
|
||||
|
||||
{list of $BREADCRUMBS with file paths}
|
||||
|
||||
## Notes
|
||||
|
||||
{any additional context from the current session}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="commit_seed">
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="confirm">
|
||||
```
|
||||
✅ Seed planted: SEED-{PADDED}
|
||||
|
||||
"{$IDEA}"
|
||||
Trigger: {$TRIGGER}
|
||||
Scope: {$SCOPE}
|
||||
File: .planning/seeds/SEED-{PADDED}-{slug}.md
|
||||
|
||||
This seed will surface automatically when you run /gsd:new-milestone
|
||||
and the milestone scope matches the trigger condition.
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Seed file created in .planning/seeds/
|
||||
- [ ] Frontmatter includes status, trigger, scope
|
||||
- [ ] Breadcrumbs collected from codebase
|
||||
- [ ] Committed to git
|
||||
- [ ] User shown confirmation with trigger info
|
||||
</success_criteria>
|
||||
129
get-shit-done/workflows/pr-branch.md
Normal file
129
get-shit-done/workflows/pr-branch.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<purpose>
|
||||
Create a clean branch for pull requests by filtering out .planning/ commits.
|
||||
The PR branch contains only code changes — reviewers don't see GSD artifacts
|
||||
(PLAN.md, SUMMARY.md, STATE.md, CONTEXT.md, etc.).
|
||||
|
||||
Uses git cherry-pick with path filtering to rebuild a clean history.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="detect_state">
|
||||
Parse `$ARGUMENTS` for target branch (default: `main`).
|
||||
|
||||
```bash
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
TARGET=${1:-main}
|
||||
```
|
||||
|
||||
Check preconditions:
|
||||
- Must be on a feature branch (not main/master)
|
||||
- Must have commits ahead of target
|
||||
|
||||
```bash
|
||||
AHEAD=$(git rev-list --count "$TARGET".."$CURRENT_BRANCH" 2>/dev/null)
|
||||
if [ "$AHEAD" = "0" ]; then
|
||||
echo "No commits ahead of $TARGET — nothing to filter."
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
Display:
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► PR BRANCH
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Branch: {CURRENT_BRANCH}
|
||||
Target: {TARGET}
|
||||
Commits: {AHEAD} ahead
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="analyze_commits">
|
||||
Classify commits:
|
||||
|
||||
```bash
|
||||
# Get all commits ahead of target
|
||||
git log --oneline "$TARGET".."$CURRENT_BRANCH" --no-merges
|
||||
```
|
||||
|
||||
For each commit, check if it ONLY touches .planning/ files:
|
||||
|
||||
```bash
|
||||
# For each commit hash
|
||||
FILES=$(git diff-tree --no-commit-id --name-only -r $HASH)
|
||||
ALL_PLANNING=$(echo "$FILES" | grep -v "^\.planning/" | wc -l)
|
||||
```
|
||||
|
||||
Classify:
|
||||
- **Code commits**: Touch at least one non-.planning/ file → INCLUDE
|
||||
- **Planning-only commits**: Touch only .planning/ files → EXCLUDE
|
||||
- **Mixed commits**: Touch both → INCLUDE (planning changes come along)
|
||||
|
||||
Display analysis:
|
||||
```
|
||||
Commits to include: {N} (code changes)
|
||||
Commits to exclude: {N} (planning-only)
|
||||
Mixed commits: {N} (code + planning — included)
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="create_pr_branch">
|
||||
```bash
|
||||
PR_BRANCH="${CURRENT_BRANCH}-pr"
|
||||
|
||||
# Create PR branch from target
|
||||
git checkout -b "$PR_BRANCH" "$TARGET"
|
||||
```
|
||||
|
||||
Cherry-pick only code commits (in order):
|
||||
|
||||
```bash
|
||||
for HASH in $CODE_COMMITS; do
|
||||
git cherry-pick "$HASH" --no-commit
|
||||
# Remove any .planning/ files that came along in mixed commits
|
||||
git rm -r --cached .planning/ 2>/dev/null || true
|
||||
git commit -C "$HASH"
|
||||
done
|
||||
```
|
||||
|
||||
Return to original branch:
|
||||
```bash
|
||||
git checkout "$CURRENT_BRANCH"
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="verify">
|
||||
```bash
|
||||
# Verify no .planning/ files in PR branch
|
||||
PLANNING_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | grep "^\.planning/" | wc -l)
|
||||
TOTAL_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | wc -l)
|
||||
PR_COMMITS=$(git rev-list --count "$TARGET".."$PR_BRANCH")
|
||||
```
|
||||
|
||||
Display results:
|
||||
```
|
||||
✅ PR branch created: {PR_BRANCH}
|
||||
|
||||
Original: {AHEAD} commits, {ORIGINAL_FILES} files
|
||||
PR branch: {PR_COMMITS} commits, {TOTAL_FILES} files
|
||||
Planning files: {PLANNING_FILES} (should be 0)
|
||||
|
||||
Next steps:
|
||||
git push origin {PR_BRANCH}
|
||||
gh pr create --base {TARGET} --head {PR_BRANCH}
|
||||
|
||||
Or use /gsd:ship to create the PR automatically.
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] PR branch created from target
|
||||
- [ ] Planning-only commits excluded
|
||||
- [ ] No .planning/ files in PR branch diff
|
||||
- [ ] Commit messages preserved from original
|
||||
- [ ] User shown next steps
|
||||
</success_criteria>
|
||||
@@ -150,17 +150,47 @@ State: "This phase has {X} plans, {Y} summaries."
|
||||
Check for UAT.md files with status "diagnosed" (has gaps needing fixes).
|
||||
|
||||
```bash
|
||||
# Check for diagnosed UAT with gaps
|
||||
grep -l "status: diagnosed" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null
|
||||
# Check for diagnosed UAT with gaps or partial (incomplete) testing
|
||||
grep -l "status: diagnosed\|status: partial" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null
|
||||
```
|
||||
|
||||
Track:
|
||||
- `uat_with_gaps`: UAT.md files with status "diagnosed" (gaps need fixing)
|
||||
- `uat_partial`: UAT.md files with status "partial" (incomplete testing)
|
||||
|
||||
**Step 1.6: Cross-phase health check**
|
||||
|
||||
Scan ALL phases in the current milestone for outstanding verification debt using the CLI (which respects milestone boundaries via `getMilestonePhaseFilter`):
|
||||
|
||||
```bash
|
||||
DEBT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw 2>/dev/null)
|
||||
```
|
||||
|
||||
Parse JSON for `summary.total_items` and `summary.total_files`.
|
||||
|
||||
Track: `outstanding_debt` — `summary.total_items` from the audit.
|
||||
|
||||
**If outstanding_debt > 0:** Add a warning section to the progress report output (in the `report` step), placed between "## What's Next" and the route suggestion:
|
||||
|
||||
```markdown
|
||||
## Verification Debt ({N} files across prior phases)
|
||||
|
||||
| Phase | File | Issue |
|
||||
|-------|------|-------|
|
||||
| {phase} | {filename} | {pending_count} pending, {skipped_count} skipped, {blocked_count} blocked |
|
||||
| {phase} | {filename} | human_needed — {count} items |
|
||||
|
||||
Review: `/gsd:audit-uat` — full cross-phase audit
|
||||
Resume testing: `/gsd:verify-work {phase}` — retest specific phase
|
||||
```
|
||||
|
||||
This is a WARNING, not a blocker — routing proceeds normally. The debt is visible so the user can make an informed choice.
|
||||
|
||||
**Step 2: Route based on counts**
|
||||
|
||||
| Condition | Meaning | Action |
|
||||
|-----------|---------|--------|
|
||||
| uat_partial > 0 | UAT testing incomplete | Go to **Route E.2** |
|
||||
| uat_with_gaps > 0 | UAT gaps need fix plans | Go to **Route E** |
|
||||
| summaries < plans | Unexecuted plans exist | Go to **Route A** |
|
||||
| summaries = plans AND plans > 0 | Phase complete | Go to Step 3 |
|
||||
@@ -260,6 +290,32 @@ UAT.md exists with gaps (diagnosed issues). User needs to plan fixes.
|
||||
|
||||
---
|
||||
|
||||
**Route E.2: UAT testing incomplete (partial)**
|
||||
|
||||
UAT.md exists with `status: partial` — testing session ended before all items resolved.
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## Incomplete UAT Testing
|
||||
|
||||
**{phase_num}-UAT.md** has {N} unresolved tests (pending, blocked, or skipped).
|
||||
|
||||
`/gsd:verify-work {phase}` — resume testing from where you left off
|
||||
|
||||
<sub>`/clear` first → fresh context window</sub>
|
||||
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- `/gsd:audit-uat` — full cross-phase UAT audit
|
||||
- `/gsd:execute-phase {phase}` — execute phase plans
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Step 3: Check milestone status (only when phase complete)**
|
||||
|
||||
Read ROADMAP.md and identify:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
228
get-shit-done/workflows/review.md
Normal file
228
get-shit-done/workflows/review.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<purpose>
|
||||
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
|
||||
Each CLI gets the same prompt (PROJECT.md context, phase plans, requirements) and
|
||||
produces structured feedback. Results are combined into REVIEWS.md for the planner
|
||||
to incorporate via --reviews flag.
|
||||
|
||||
This implements adversarial review: different AI models catch different blind spots.
|
||||
A plan that survives review from 2-3 independent AI systems is more robust.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="detect_clis">
|
||||
Check which AI CLIs are available on the system:
|
||||
|
||||
```bash
|
||||
# Check each CLI
|
||||
command -v gemini >/dev/null 2>&1 && echo "gemini:available" || echo "gemini:missing"
|
||||
command -v claude >/dev/null 2>&1 && echo "claude:available" || echo "claude:missing"
|
||||
command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missing"
|
||||
```
|
||||
|
||||
Parse flags from `$ARGUMENTS`:
|
||||
- `--gemini` → include Gemini
|
||||
- `--claude` → include Claude
|
||||
- `--codex` → include Codex
|
||||
- `--all` → include all available
|
||||
- No flags → include all available
|
||||
|
||||
If no CLIs are available:
|
||||
```
|
||||
No external AI CLIs found. Install at least one:
|
||||
- gemini: https://github.com/google-gemini/gemini-cli
|
||||
- codex: https://github.com/openai/codex
|
||||
- claude: https://github.com/anthropics/claude-code
|
||||
|
||||
Then run /gsd:review again.
|
||||
```
|
||||
Exit.
|
||||
|
||||
If only one CLI is the current runtime (e.g. running inside Claude), skip it for the review
|
||||
to ensure independence. At least one DIFFERENT CLI must be available.
|
||||
</step>
|
||||
|
||||
<step name="gather_context">
|
||||
Collect phase artifacts for the review prompt:
|
||||
|
||||
```bash
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Read from init: `phase_dir`, `phase_number`, `padded_phase`.
|
||||
|
||||
Then read:
|
||||
1. `.planning/PROJECT.md` (first 80 lines — project context)
|
||||
2. Phase section from `.planning/ROADMAP.md`
|
||||
3. All `*-PLAN.md` files in the phase directory
|
||||
4. `*-CONTEXT.md` if present (user decisions)
|
||||
5. `*-RESEARCH.md` if present (domain research)
|
||||
6. `.planning/REQUIREMENTS.md` (requirements this phase addresses)
|
||||
</step>
|
||||
|
||||
<step name="build_prompt">
|
||||
Build a structured review prompt:
|
||||
|
||||
```markdown
|
||||
# Cross-AI Plan Review Request
|
||||
|
||||
You are reviewing implementation plans for a software project phase.
|
||||
Provide structured feedback on plan quality, completeness, and risks.
|
||||
|
||||
## Project Context
|
||||
{first 80 lines of PROJECT.md}
|
||||
|
||||
## Phase {N}: {phase name}
|
||||
### Roadmap Section
|
||||
{roadmap phase section}
|
||||
|
||||
### Requirements Addressed
|
||||
{requirements for this phase}
|
||||
|
||||
### User Decisions (CONTEXT.md)
|
||||
{context if present}
|
||||
|
||||
### Research Findings
|
||||
{research if present}
|
||||
|
||||
### Plans to Review
|
||||
{all PLAN.md contents}
|
||||
|
||||
## Review Instructions
|
||||
|
||||
Analyze each plan and provide:
|
||||
|
||||
1. **Summary** — One-paragraph assessment
|
||||
2. **Strengths** — What's well-designed (bullet points)
|
||||
3. **Concerns** — Potential issues, gaps, risks (bullet points with severity: HIGH/MEDIUM/LOW)
|
||||
4. **Suggestions** — Specific improvements (bullet points)
|
||||
5. **Risk Assessment** — Overall risk level (LOW/MEDIUM/HIGH) with justification
|
||||
|
||||
Focus on:
|
||||
- Missing edge cases or error handling
|
||||
- Dependency ordering issues
|
||||
- Scope creep or over-engineering
|
||||
- Security considerations
|
||||
- Performance implications
|
||||
- Whether the plans actually achieve the phase goals
|
||||
|
||||
Output your review in markdown format.
|
||||
```
|
||||
|
||||
Write to a temp file: `/tmp/gsd-review-prompt-{phase}.md`
|
||||
</step>
|
||||
|
||||
<step name="invoke_reviewers">
|
||||
For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
|
||||
|
||||
**Gemini:**
|
||||
```bash
|
||||
gemini -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-gemini-{phase}.md
|
||||
```
|
||||
|
||||
**Claude (separate session):**
|
||||
```bash
|
||||
claude -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" --no-input 2>/dev/null > /tmp/gsd-review-claude-{phase}.md
|
||||
```
|
||||
|
||||
**Codex:**
|
||||
```bash
|
||||
codex -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
|
||||
```
|
||||
|
||||
If a CLI fails, log the error and continue with remaining CLIs.
|
||||
|
||||
Display progress:
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► CROSS-AI REVIEW — Phase {N}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
◆ Reviewing with {CLI}... done ✓
|
||||
◆ Reviewing with {CLI}... done ✓
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="write_reviews">
|
||||
Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
phase: {N}
|
||||
reviewers: [gemini, claude, codex]
|
||||
reviewed_at: {ISO timestamp}
|
||||
plans_reviewed: [{list of PLAN.md files}]
|
||||
---
|
||||
|
||||
# Cross-AI Plan Review — Phase {N}
|
||||
|
||||
## Gemini Review
|
||||
|
||||
{gemini review content}
|
||||
|
||||
---
|
||||
|
||||
## Claude Review
|
||||
|
||||
{claude review content}
|
||||
|
||||
---
|
||||
|
||||
## Codex Review
|
||||
|
||||
{codex review content}
|
||||
|
||||
---
|
||||
|
||||
## Consensus Summary
|
||||
|
||||
{synthesize common concerns across all reviewers}
|
||||
|
||||
### Agreed Strengths
|
||||
{strengths mentioned by 2+ reviewers}
|
||||
|
||||
### Agreed Concerns
|
||||
{concerns raised by 2+ reviewers — highest priority}
|
||||
|
||||
### Divergent Views
|
||||
{where reviewers disagreed — worth investigating}
|
||||
```
|
||||
|
||||
Commit:
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: cross-AI review for phase {N}" --files {phase_dir}/{padded_phase}-REVIEWS.md
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="present_results">
|
||||
Display summary:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► REVIEW COMPLETE
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Phase {N} reviewed by {count} AI systems.
|
||||
|
||||
Consensus concerns:
|
||||
{top 3 shared concerns}
|
||||
|
||||
Full review: {padded_phase}-REVIEWS.md
|
||||
|
||||
To incorporate feedback into planning:
|
||||
/gsd:plan-phase {N} --reviews
|
||||
```
|
||||
|
||||
Clean up temp files.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] At least one external CLI invoked successfully
|
||||
- [ ] REVIEWS.md written with structured feedback
|
||||
- [ ] Consensus summary synthesized from multiple reviewers
|
||||
- [ ] Temp files cleaned up
|
||||
- [ ] User knows how to use feedback (/gsd:plan-phase --reviews)
|
||||
</success_criteria>
|
||||
146
get-shit-done/workflows/session-report.md
Normal file
146
get-shit-done/workflows/session-report.md
Normal file
@@ -0,0 +1,146 @@
|
||||
<purpose>
|
||||
Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="gather_session_data">
|
||||
Collect session data from available sources:
|
||||
|
||||
1. **STATE.md** — current phase, milestone, progress, blockers, decisions
|
||||
2. **Git log** — commits made during this session (last 24h or since last report)
|
||||
3. **Plan/Summary files** — plans executed, summaries written
|
||||
4. **ROADMAP.md** — milestone context and phase goals
|
||||
|
||||
```bash
|
||||
# Get recent commits (last 24 hours)
|
||||
git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits"
|
||||
|
||||
# Count files changed
|
||||
git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available"
|
||||
```
|
||||
|
||||
Read `.planning/STATE.md` to get:
|
||||
- Current milestone and phase
|
||||
- Progress percentage
|
||||
- Active blockers
|
||||
- Recent decisions
|
||||
|
||||
Read `.planning/ROADMAP.md` to get milestone name and goals.
|
||||
|
||||
Check for existing reports:
|
||||
```bash
|
||||
ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports"
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="estimate_usage">
|
||||
Estimate token usage from observable signals:
|
||||
|
||||
- Count of tool calls is not directly available, so estimate from git activity and file operations
|
||||
- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks
|
||||
|
||||
Estimation heuristics:
|
||||
- Each commit ≈ 1 plan cycle (research + plan + execute + verify)
|
||||
- Each plan file ≈ 2,000-5,000 tokens of agent context
|
||||
- Each summary file ≈ 1,000-2,000 tokens generated
|
||||
- Subagent spawns multiply by ~1.5x per agent type used
|
||||
</step>
|
||||
|
||||
<step name="generate_report">
|
||||
Create the report directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p .planning/reports
|
||||
```
|
||||
|
||||
Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist):
|
||||
|
||||
```markdown
|
||||
# GSD Session Report
|
||||
|
||||
**Generated:** [timestamp]
|
||||
**Project:** [from PROJECT.md title or directory name]
|
||||
**Milestone:** [N] — [milestone name from ROADMAP.md]
|
||||
|
||||
---
|
||||
|
||||
## Session Summary
|
||||
|
||||
**Duration:** [estimated from first to last commit timestamp, or "Single session"]
|
||||
**Phase Progress:** [from STATE.md]
|
||||
**Plans Executed:** [count of summaries written this session]
|
||||
**Commits Made:** [count from git log]
|
||||
|
||||
## Work Performed
|
||||
|
||||
### Phases Touched
|
||||
[List phases worked on with brief description of what was done]
|
||||
|
||||
### Key Outcomes
|
||||
[Bullet list of concrete deliverables: files created, features implemented, bugs fixed]
|
||||
|
||||
### Decisions Made
|
||||
[From STATE.md decisions table, if any were added this session]
|
||||
|
||||
## Files Changed
|
||||
|
||||
[Summary of files modified, created, deleted — from git diff stat]
|
||||
|
||||
## Blockers & Open Items
|
||||
|
||||
[Active blockers from STATE.md]
|
||||
[Any TODO items created during session]
|
||||
|
||||
## Estimated Resource Usage
|
||||
|
||||
| Metric | Estimate |
|
||||
|--------|----------|
|
||||
| Commits | [N] |
|
||||
| Files changed | [N] |
|
||||
| Plans executed | [N] |
|
||||
| Subagents spawned | [estimated] |
|
||||
|
||||
> **Note:** Token and cost estimates require API-level instrumentation.
|
||||
> These metrics reflect observable session activity only.
|
||||
|
||||
---
|
||||
|
||||
*Generated by `/gsd:session-report`*
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="display_result">
|
||||
Show the user:
|
||||
|
||||
```
|
||||
## Session Report Generated
|
||||
|
||||
📄 `.planning/reports/[filename].md`
|
||||
|
||||
### Highlights
|
||||
- **Commits:** [N]
|
||||
- **Files changed:** [N]
|
||||
- **Phase progress:** [X]%
|
||||
- **Plans executed:** [N]
|
||||
```
|
||||
|
||||
If this is the first report, mention:
|
||||
```
|
||||
💡 Run `/gsd:session-report` at the end of each session to build a history of project activity.
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Session data gathered from STATE.md, git log, and plan files
|
||||
- [ ] Report written to .planning/reports/
|
||||
- [ ] Report includes work summary, outcomes, and file changes
|
||||
- [ ] Filename includes date to prevent overwrites
|
||||
- [ ] Result summary displayed to user
|
||||
</success_criteria>
|
||||
@@ -49,7 +49,7 @@ AskUserQuestion([
|
||||
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
|
||||
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" },
|
||||
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
|
||||
{ label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" }
|
||||
{ label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -135,6 +135,15 @@ AskUserQuestion([
|
||||
{ label: "Yes (Recommended)", description: "Warn when context usage exceeds 65%. Helps avoid losing work." },
|
||||
{ label: "No", description: "Disable warnings. Allows Claude to reach auto-compact naturally. Good for long unattended runs." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Research best practices before asking questions? (web search during new-project and discuss-phase)",
|
||||
header: "Research Qs",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (Recommended)", description: "Ask questions directly. Faster, uses fewer tokens." },
|
||||
{ label: "Yes", description: "Search web for best practices before each question group. More informed questions but uses more tokens." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
@@ -157,10 +166,16 @@ Merge new settings into existing config.json:
|
||||
"ui_safety_gate": true/false
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none" | "phase" | "milestone"
|
||||
"branching_strategy": "none" | "phase" | "milestone",
|
||||
"quick_branch_template": <string|null>
|
||||
},
|
||||
"hooks": {
|
||||
"context_warnings": true/false
|
||||
"context_warnings": true/false,
|
||||
"workflow_guard": true/false,
|
||||
"research_questions": true/false
|
||||
},
|
||||
"workflow": {
|
||||
"text_mode": true/false // Use plain-text questions instead of TUI menus (for /rc remote sessions)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -200,6 +215,7 @@ Write `~/.gsd/defaults.json` with:
|
||||
"commit_docs": <current>,
|
||||
"parallelization": <current>,
|
||||
"branching_strategy": <current>,
|
||||
"quick_branch_template": <current>,
|
||||
"workflow": {
|
||||
"research": <current>,
|
||||
"plan_check": <current>,
|
||||
|
||||
228
get-shit-done/workflows/ship.md
Normal file
228
get-shit-done/workflows/ship.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<purpose>
|
||||
Create a pull request from completed phase/milestone work, generate a rich PR body from planning artifacts, optionally run code review, and prepare for merge. Closes the plan → execute → verify → ship loop.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="initialize">
|
||||
Parse arguments and load project state:
|
||||
|
||||
```bash
|
||||
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
|
||||
|
||||
Also load config for branching strategy:
|
||||
```bash
|
||||
CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
|
||||
```
|
||||
|
||||
Extract: `branching_strategy`, `branch_name`.
|
||||
</step>
|
||||
|
||||
<step name="preflight_checks">
|
||||
Verify the work is ready to ship:
|
||||
|
||||
1. **Verification passed?**
|
||||
```bash
|
||||
VERIFICATION=$(cat ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null)
|
||||
```
|
||||
Check for `status: passed` or `status: human_needed` (with human approval).
|
||||
If no VERIFICATION.md or status is `gaps_found`: warn and ask user to confirm.
|
||||
|
||||
2. **Clean working tree?**
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
If uncommitted changes exist: ask user to commit or stash first.
|
||||
|
||||
3. **On correct branch?**
|
||||
```bash
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
```
|
||||
If on `main`/`master`: warn — should be on a feature branch.
|
||||
If branching_strategy is `none`: offer to create a branch now.
|
||||
|
||||
4. **Remote configured?**
|
||||
```bash
|
||||
git remote -v | head -2
|
||||
```
|
||||
Detect `origin` remote. If no remote: error — can't create PR.
|
||||
|
||||
5. **`gh` CLI available?**
|
||||
```bash
|
||||
which gh && gh auth status 2>&1
|
||||
```
|
||||
If `gh` not found or not authenticated: provide setup instructions and exit.
|
||||
</step>
|
||||
|
||||
<step name="push_branch">
|
||||
Push the current branch to remote:
|
||||
|
||||
```bash
|
||||
git push origin ${CURRENT_BRANCH} 2>&1
|
||||
```
|
||||
|
||||
If push fails (e.g., no upstream): set upstream:
|
||||
```bash
|
||||
git push --set-upstream origin ${CURRENT_BRANCH} 2>&1
|
||||
```
|
||||
|
||||
Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)"
|
||||
</step>
|
||||
|
||||
<step name="generate_pr_body">
|
||||
Auto-generate a rich PR body from planning artifacts:
|
||||
|
||||
**1. Title:**
|
||||
```
|
||||
Phase {phase_number}: {phase_name}
|
||||
```
|
||||
Or for milestone: `Milestone {version}: {name}`
|
||||
|
||||
**2. Summary section:**
|
||||
Read ROADMAP.md for phase goal. Read VERIFICATION.md for verification status.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
**Phase {N}: {Name}**
|
||||
**Goal:** {goal from ROADMAP.md}
|
||||
**Status:** Verified ✓
|
||||
|
||||
{One paragraph synthesized from SUMMARY.md files — what was built}
|
||||
```
|
||||
|
||||
**3. Changes section:**
|
||||
For each SUMMARY.md in the phase directory:
|
||||
```markdown
|
||||
## Changes
|
||||
|
||||
### Plan {plan_id}: {plan_name}
|
||||
{one_liner from SUMMARY.md frontmatter}
|
||||
|
||||
**Key files:**
|
||||
{key-files.created and key-files.modified from SUMMARY.md frontmatter}
|
||||
```
|
||||
|
||||
**4. Requirements section:**
|
||||
```markdown
|
||||
## Requirements Addressed
|
||||
|
||||
{REQ-IDs from plan frontmatter, linked to REQUIREMENTS.md descriptions}
|
||||
```
|
||||
|
||||
**5. Testing section:**
|
||||
```markdown
|
||||
## Verification
|
||||
|
||||
- [x] Automated verification: {pass/fail from VERIFICATION.md}
|
||||
- {human verification items from VERIFICATION.md, if any}
|
||||
```
|
||||
|
||||
**6. Decisions section:**
|
||||
```markdown
|
||||
## Key Decisions
|
||||
|
||||
{Decisions from STATE.md accumulated context relevant to this phase}
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="create_pr">
|
||||
Create the PR using the generated body:
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
|
||||
--body "${PR_BODY}" \
|
||||
--base main
|
||||
```
|
||||
|
||||
If `--draft` flag was passed: add `--draft`.
|
||||
|
||||
Report: "PR #{number} created: {url}"
|
||||
</step>
|
||||
|
||||
<step name="optional_review">
|
||||
Ask if user wants to trigger a code review:
|
||||
|
||||
```
|
||||
AskUserQuestion:
|
||||
question: "PR created. Run a code review before merge?"
|
||||
options:
|
||||
- label: "Skip review"
|
||||
description: "PR is ready — merge when CI passes"
|
||||
- label: "Self-review"
|
||||
description: "I'll review the diff in the PR myself"
|
||||
- label: "Request review"
|
||||
description: "Request review from a teammate"
|
||||
```
|
||||
|
||||
**If "Request review":**
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --add-reviewer "${REVIEWER}"
|
||||
```
|
||||
|
||||
**If "Self-review":**
|
||||
Report the PR URL and suggest: "Review the diff at {url}/files"
|
||||
</step>
|
||||
|
||||
<step name="track_shipping">
|
||||
Update STATE.md to reflect the shipping action:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)"
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}"
|
||||
```
|
||||
|
||||
If `commit_docs` is true:
|
||||
```bash
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="report">
|
||||
```
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
## ✓ Phase {X}: {Name} — Shipped
|
||||
|
||||
PR: #{number} ({url})
|
||||
Branch: {branch} → main
|
||||
Commits: {count}
|
||||
Verification: ✓ Passed
|
||||
Requirements: {N} REQ-IDs addressed
|
||||
|
||||
Next steps:
|
||||
- Review/approve PR
|
||||
- Merge when CI passes
|
||||
- /gsd:complete-milestone (if last phase in milestone)
|
||||
- /gsd:progress (to see what's next)
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<offer_next>
|
||||
After shipping:
|
||||
|
||||
- /gsd:complete-milestone — if all phases in milestone are done
|
||||
- /gsd:progress — see overall project state
|
||||
- /gsd:execute-phase {next} — continue to next phase
|
||||
</offer_next>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Preflight checks passed (verification, clean tree, branch, remote, gh)
|
||||
- [ ] Branch pushed to remote
|
||||
- [ ] PR created with rich auto-generated body
|
||||
- [ ] STATE.md updated with shipping status
|
||||
- [ ] User knows PR number and next steps
|
||||
</success_criteria>
|
||||
@@ -1,3 +1,19 @@
|
||||
<internal_workflow>
|
||||
|
||||
**This is an INTERNAL workflow — NOT a user-facing command.**
|
||||
|
||||
There is no `/gsd:transition` command. This workflow is invoked automatically by
|
||||
`execute-phase` during auto-advance, or inline by the orchestrator after phase
|
||||
verification. Users should never be told to run `/gsd:transition`.
|
||||
|
||||
**Valid user commands for phase progression:**
|
||||
- `/gsd:discuss-phase {N}` — discuss a phase before planning
|
||||
- `/gsd:plan-phase {N}` — plan a phase
|
||||
- `/gsd:execute-phase {N}` — execute a phase
|
||||
- `/gsd:progress` — see roadmap progress
|
||||
|
||||
</internal_workflow>
|
||||
|
||||
<required_reading>
|
||||
|
||||
**Read these files NOW:**
|
||||
@@ -58,6 +74,30 @@ cat .planning/config.json 2>/dev/null
|
||||
|
||||
</config-check>
|
||||
|
||||
**Check for verification debt in this phase:**
|
||||
|
||||
```bash
|
||||
# Count outstanding items in current phase
|
||||
OUTSTANDING=""
|
||||
for f in .planning/phases/XX-current/*-UAT.md .planning/phases/XX-current/*-VERIFICATION.md; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -q "result: pending\|result: blocked\|status: partial\|status: human_needed\|status: diagnosed" "$f" && OUTSTANDING="$OUTSTANDING\n$(basename $f)"
|
||||
done
|
||||
```
|
||||
|
||||
**If OUTSTANDING is not empty:**
|
||||
|
||||
Append to the completion confirmation message (regardless of mode):
|
||||
|
||||
```
|
||||
Outstanding verification items in this phase:
|
||||
{list filenames}
|
||||
|
||||
These will carry forward as debt. Review: `/gsd:audit-uat`
|
||||
```
|
||||
|
||||
This does NOT block transition — it ensures the user sees the debt before confirming.
|
||||
|
||||
**If all plans complete:**
|
||||
|
||||
<if mode="yolo">
|
||||
|
||||
@@ -20,8 +20,11 @@ First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context`
|
||||
Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it.
|
||||
|
||||
```bash
|
||||
# Runtime candidates: "<runtime>:<config-dir>"
|
||||
RUNTIME_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex"
|
||||
# Runtime candidates: "<runtime>:<config-dir>" stored as an array.
|
||||
# Using an array instead of a space-separated string ensures correct
|
||||
# iteration in both bash and zsh (zsh does not word-split unquoted
|
||||
# variables by default). Fixes #1173.
|
||||
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" )
|
||||
|
||||
# PREFERRED_RUNTIME should be set from execution_context before running this block.
|
||||
# If not set, infer from runtime env vars; fallback to claude.
|
||||
@@ -40,23 +43,23 @@ if [ -z "$PREFERRED_RUNTIME" ]; then
|
||||
fi
|
||||
|
||||
# Reorder entries so preferred runtime is checked first.
|
||||
ORDERED_RUNTIME_DIRS=""
|
||||
for entry in $RUNTIME_DIRS; do
|
||||
ORDERED_RUNTIME_DIRS=()
|
||||
for entry in "${RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then
|
||||
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
|
||||
ORDERED_RUNTIME_DIRS+=( "$entry" )
|
||||
fi
|
||||
done
|
||||
for entry in $RUNTIME_DIRS; do
|
||||
for entry in "${RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then
|
||||
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
|
||||
ORDERED_RUNTIME_DIRS+=( "$entry" )
|
||||
fi
|
||||
done
|
||||
|
||||
# Check local first (takes priority only if valid and distinct from global)
|
||||
LOCAL_VERSION_FILE="" LOCAL_MARKER_FILE="" LOCAL_DIR="" LOCAL_RUNTIME=""
|
||||
for entry in $ORDERED_RUNTIME_DIRS; do
|
||||
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
dir="${entry#*:}"
|
||||
if [ -f "./$dir/get-shit-done/VERSION" ] || [ -f "./$dir/get-shit-done/workflows/update.md" ]; then
|
||||
@@ -69,7 +72,7 @@ for entry in $ORDERED_RUNTIME_DIRS; do
|
||||
done
|
||||
|
||||
GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME=""
|
||||
for entry in $ORDERED_RUNTIME_DIRS; do
|
||||
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
|
||||
runtime="${entry%%:*}"
|
||||
dir="${entry#*:}"
|
||||
if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then
|
||||
|
||||
@@ -231,6 +231,29 @@ result: skipped
|
||||
reason: [user's reason if provided]
|
||||
```
|
||||
|
||||
**If response indicates blocked:**
|
||||
- "blocked", "can't test - server not running", "need physical device", "need release build"
|
||||
- Or any response containing: "server", "blocked", "not running", "physical device", "release build"
|
||||
|
||||
Infer blocked_by tag from response:
|
||||
- Contains: server, not running, gateway, API → `server`
|
||||
- Contains: physical, device, hardware, real phone → `physical-device`
|
||||
- Contains: release, preview, build, EAS → `release-build`
|
||||
- Contains: stripe, twilio, third-party, configure → `third-party`
|
||||
- Contains: depends on, prior phase, prerequisite → `prior-phase`
|
||||
- Default: `other`
|
||||
|
||||
Update Tests section:
|
||||
```
|
||||
### {N}. {name}
|
||||
expected: {expected}
|
||||
result: blocked
|
||||
blocked_by: {inferred tag}
|
||||
reason: "{verbatim user response}"
|
||||
```
|
||||
|
||||
Note: Blocked tests do NOT go into the Gaps section (they aren't code issues — they're prerequisite gates).
|
||||
|
||||
**If response is anything else:**
|
||||
- Treat as issue description
|
||||
|
||||
@@ -293,8 +316,24 @@ Proceed to `present_test`.
|
||||
<step name="complete_session">
|
||||
**Complete testing and commit:**
|
||||
|
||||
**Determine final status:**
|
||||
|
||||
Count results:
|
||||
- `pending_count`: tests with `result: [pending]`
|
||||
- `blocked_count`: tests with `result: blocked`
|
||||
- `skipped_no_reason`: tests with `result: skipped` and no `reason` field
|
||||
|
||||
```
|
||||
if pending_count > 0 OR blocked_count > 0 OR skipped_no_reason > 0:
|
||||
status: partial
|
||||
# Session ended but not all tests resolved
|
||||
else:
|
||||
status: complete
|
||||
# All tests have a definitive result (pass, issue, or skipped-with-reason)
|
||||
```
|
||||
|
||||
Update frontmatter:
|
||||
- status: complete
|
||||
- status: {computed status}
|
||||
- updated: [now]
|
||||
|
||||
Clear Current Test section:
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
93
hooks/gsd-workflow-guard.js
Normal file
93
hooks/gsd-workflow-guard.js
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
// GSD Workflow Guard — PreToolUse hook
|
||||
// Detects when Claude attempts file edits outside a GSD workflow context
|
||||
// (no active /gsd: command or Task subagent) and injects an advisory warning.
|
||||
//
|
||||
// This is a SOFT guard — it advises, not blocks. The edit still proceeds.
|
||||
// The warning nudges Claude to use /gsd:quick or /gsd:fast instead of
|
||||
// making direct edits that bypass state tracking.
|
||||
//
|
||||
// Enable via config: hooks.workflow_guard: true (default: false)
|
||||
// Only triggers on Write/Edit tool calls to non-.planning/ files.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let input = '';
|
||||
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
clearTimeout(stdinTimeout);
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
const toolName = data.tool_name;
|
||||
|
||||
// Only guard Write and Edit tool calls
|
||||
if (toolName !== 'Write' && toolName !== 'Edit') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if we're inside a GSD workflow (Task subagent or /gsd: command)
|
||||
// Subagents have a session_id that differs from the parent
|
||||
// and typically have a description field set by the orchestrator
|
||||
if (data.tool_input?.is_subagent || data.session_type === 'task') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check the file being edited
|
||||
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
||||
|
||||
// Allow edits to .planning/ files (GSD state management)
|
||||
if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Allow edits to common config/docs files that don't need GSD tracking
|
||||
const allowedPatterns = [
|
||||
/\.gitignore$/,
|
||||
/\.env/,
|
||||
/CLAUDE\.md$/,
|
||||
/AGENTS\.md$/,
|
||||
/GEMINI\.md$/,
|
||||
/settings\.json$/,
|
||||
];
|
||||
if (allowedPatterns.some(p => p.test(filePath))) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if workflow guard is enabled
|
||||
const cwd = data.cwd || process.cwd();
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
if (!config.hooks?.workflow_guard) {
|
||||
process.exit(0); // Guard disabled (default)
|
||||
}
|
||||
} catch (e) {
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
process.exit(0); // No GSD project — don't guard
|
||||
}
|
||||
|
||||
// If we get here: GSD project, guard enabled, file edit outside .planning/,
|
||||
// not in a subagent context. Inject advisory warning.
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
|
||||
'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
|
||||
'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
|
||||
'to maintain project state tracking. ' +
|
||||
'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.'
|
||||
}
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
} catch (e) {
|
||||
// Silent fail — never block tool execution
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -151,6 +151,20 @@ describe('SPAWN: spawn type consistency', () => {
|
||||
'diagnose-issues should spawn gsd-debugger, not general-purpose'
|
||||
);
|
||||
});
|
||||
|
||||
test('execute-phase has Copilot sequential fallback in runtime_compatibility', () => {
|
||||
const content = fs.readFileSync(
|
||||
path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('sequential inline execution'),
|
||||
'execute-phase must document sequential inline execution as Copilot fallback'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('spot-check'),
|
||||
'execute-phase must have spot-check fallback for completion detection'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Required Frontmatter Fields ─────────────────────────────────────────────
|
||||
@@ -167,3 +181,34 @@ describe('AGENT: required frontmatter fields', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Discussion Log ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('DISCUSS: discussion log generation', () => {
|
||||
test('discuss-phase workflow references DISCUSSION-LOG.md generation', () => {
|
||||
const content = fs.readFileSync(
|
||||
path.join(WORKFLOWS_DIR, 'discuss-phase.md'), 'utf-8'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('DISCUSSION-LOG.md'),
|
||||
'discuss-phase must reference DISCUSSION-LOG.md generation'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('Audit trail only'),
|
||||
'discuss-phase must mark discussion log as audit-only'
|
||||
);
|
||||
});
|
||||
|
||||
test('discussion-log template exists', () => {
|
||||
const templatePath = path.join(__dirname, '..', 'get-shit-done', 'templates', 'discussion-log.md');
|
||||
assert.ok(
|
||||
fs.existsSync(templatePath),
|
||||
'discussion-log.md template must exist'
|
||||
);
|
||||
const content = fs.readFileSync(templatePath, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('Do not use as input to planning'),
|
||||
'template must contain audit-only notice'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
82
tests/claude-md.test.cjs
Normal file
82
tests/claude-md.test.cjs
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* CLAUDE.md generation and new-project workflow tests
|
||||
*/
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
describe('generate-claude-md', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('creates CLAUDE.md with workflow enforcement section', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'PROJECT.md'),
|
||||
'# Test Project\n\n## What This Is\n\nA small test project.\n'
|
||||
);
|
||||
|
||||
const result = runGsdTools('generate-claude-md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.action, 'created');
|
||||
assert.strictEqual(output.sections_total, 5);
|
||||
assert.ok(output.sections_generated.includes('workflow'));
|
||||
|
||||
const claudePath = path.join(tmpDir, 'CLAUDE.md');
|
||||
const content = fs.readFileSync(claudePath, 'utf-8');
|
||||
assert.ok(content.includes('## GSD Workflow Enforcement'));
|
||||
assert.ok(content.includes('/gsd:quick'));
|
||||
assert.ok(content.includes('/gsd:debug'));
|
||||
assert.ok(content.includes('/gsd:execute-phase'));
|
||||
assert.ok(content.includes('Do not make direct repo edits outside a GSD workflow'));
|
||||
});
|
||||
|
||||
test('adds workflow enforcement section when updating an existing CLAUDE.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'PROJECT.md'),
|
||||
'# Test Project\n\n## What This Is\n\nA small test project.\n'
|
||||
);
|
||||
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '## Local Notes\n\nKeep this intro.\n');
|
||||
|
||||
const result = runGsdTools('generate-claude-md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.action, 'updated');
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
||||
assert.ok(content.includes('## Local Notes'));
|
||||
assert.ok(content.includes('## GSD Workflow Enforcement'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('new-project workflow includes CLAUDE.md generation', () => {
|
||||
const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'new-project.md');
|
||||
const commandsPath = path.join(__dirname, '..', 'docs', 'COMMANDS.md');
|
||||
|
||||
test('new-project workflow generates CLAUDE.md before final commit', () => {
|
||||
const content = fs.readFileSync(workflowPath, 'utf-8');
|
||||
assert.ok(content.includes('generate-claude-md'));
|
||||
assert.ok(content.includes('--files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md'));
|
||||
});
|
||||
|
||||
test('new-project artifacts mention CLAUDE.md', () => {
|
||||
const workflowContent = fs.readFileSync(workflowPath, 'utf-8');
|
||||
const commandsContent = fs.readFileSync(commandsPath, 'utf-8');
|
||||
|
||||
assert.ok(workflowContent.includes('| Project guide | `CLAUDE.md`'));
|
||||
assert.ok(workflowContent.includes('- `CLAUDE.md`'));
|
||||
assert.ok(commandsContent.includes('`CLAUDE.md`'));
|
||||
});
|
||||
});
|
||||
@@ -160,6 +160,19 @@ tools: Read, Grep, Glob
|
||||
assert.ok(result.includes("'''"), 'has closing literal triple quotes');
|
||||
});
|
||||
|
||||
test('includes required name and description fields', () => {
|
||||
const result = generateCodexAgentToml('gsd-executor', sampleAgent);
|
||||
assert.ok(result.includes('name = "gsd-executor"'), 'has name');
|
||||
assert.ok(result.includes('description = "Executes plans"'), 'has description');
|
||||
});
|
||||
|
||||
test('falls back to generated description when frontmatter is missing fields', () => {
|
||||
const minimalAgent = `<role>You are an unknown agent.</role>`;
|
||||
const result = generateCodexAgentToml('gsd-unknown', minimalAgent);
|
||||
assert.ok(result.includes('name = "gsd-unknown"'), 'falls back to agent name');
|
||||
assert.ok(result.includes('description = "GSD agent gsd-unknown"'), 'falls back to synthetic description');
|
||||
});
|
||||
|
||||
test('defaults unknown agents to read-only', () => {
|
||||
const result = generateCodexAgentToml('gsd-unknown', sampleAgent);
|
||||
assert.ok(result.includes('sandbox_mode = "read-only"'), 'defaults to read-only');
|
||||
@@ -354,6 +367,36 @@ describe('mergeCodexConfig', () => {
|
||||
assert.ok(content.includes('[agents.gsd-executor]'), 'has agent');
|
||||
});
|
||||
|
||||
test('case 3 strips existing [agents.gsd-*] sections before appending fresh block', () => {
|
||||
const configPath = path.join(tmpDir, 'config.toml');
|
||||
const existing = [
|
||||
'[model]',
|
||||
'name = "o3"',
|
||||
'',
|
||||
'[agents.custom-agent]',
|
||||
'description = "user agent"',
|
||||
'',
|
||||
'',
|
||||
'[agents.gsd-executor]',
|
||||
'description = "old"',
|
||||
'config_file = "agents/gsd-executor.toml"',
|
||||
'',
|
||||
].join('\n');
|
||||
fs.writeFileSync(configPath, existing);
|
||||
|
||||
mergeCodexConfig(configPath, sampleBlock);
|
||||
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const gsdAgentCount = (content.match(/^\[agents\.gsd-executor\]\s*$/gm) || []).length;
|
||||
const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
||||
|
||||
assert.ok(content.includes('[model]'), 'preserves user content');
|
||||
assert.ok(content.includes('[agents.custom-agent]'), 'preserves non-GSD agent section');
|
||||
assert.strictEqual(gsdAgentCount, 1, 'keeps exactly one GSD agent section');
|
||||
assert.strictEqual(markerCount, 1, 'adds exactly one marker block');
|
||||
assert.ok(!/\n{3,}# GSD Agent Configuration/.test(content), 'does not leave extra blank lines before marker block');
|
||||
});
|
||||
|
||||
test('idempotent: re-merge produces same result', () => {
|
||||
const configPath = path.join(tmpDir, 'config.toml');
|
||||
mergeCodexConfig(configPath, sampleBlock);
|
||||
@@ -485,10 +528,47 @@ describe('installCodexConfig (integration)', () => {
|
||||
assert.ok(fs.existsSync(path.join(agentsDir, 'gsd-plan-checker.toml')), 'plan-checker .toml exists');
|
||||
|
||||
const executorToml = fs.readFileSync(path.join(agentsDir, 'gsd-executor.toml'), 'utf8');
|
||||
assert.ok(executorToml.includes('name = "gsd-executor"'), 'executor has name');
|
||||
assert.ok(executorToml.includes('description = "Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command."'), 'executor has description');
|
||||
assert.ok(executorToml.includes('sandbox_mode = "workspace-write"'), 'executor is workspace-write');
|
||||
assert.ok(executorToml.includes('developer_instructions'), 'has developer_instructions');
|
||||
|
||||
const checkerToml = fs.readFileSync(path.join(agentsDir, 'gsd-plan-checker.toml'), 'utf8');
|
||||
assert.ok(checkerToml.includes('name = "gsd-plan-checker"'), 'plan-checker has name');
|
||||
assert.ok(checkerToml.includes('sandbox_mode = "read-only"'), 'plan-checker is read-only');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Codex config.toml [features] safety (#1202) ─────────────────────────────
|
||||
|
||||
describe('codex features section safety', () => {
|
||||
test('non-boolean keys under [features] are moved to top level', () => {
|
||||
// Simulate the bug from #1202: model = "gpt-5.4" under [features]
|
||||
// causes "invalid type: string, expected a boolean in features"
|
||||
const configContent = `[features]\ncodex_hooks = true\n\nmodel = "gpt-5.4"\nmodel_reasoning_effort = "medium"\n\n[agents.gsd-executor]\ndescription = "test"\n`;
|
||||
|
||||
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
|
||||
assert.ok(featuresMatch, 'features section found');
|
||||
|
||||
const featuresBody = featuresMatch[1];
|
||||
const nonBooleanKeys = featuresBody.split('\n')
|
||||
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
|
||||
.map(line => line.trim());
|
||||
|
||||
assert.strictEqual(nonBooleanKeys.length, 2, 'should detect 2 non-boolean keys');
|
||||
assert.ok(nonBooleanKeys.includes('model = "gpt-5.4"'), 'detects model key');
|
||||
assert.ok(nonBooleanKeys.includes('model_reasoning_effort = "medium"'), 'detects model_reasoning_effort key');
|
||||
});
|
||||
|
||||
test('boolean keys under [features] are NOT flagged', () => {
|
||||
const configContent = `[features]\ncodex_hooks = true\nmulti_agent = false\n`;
|
||||
|
||||
const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
|
||||
const featuresBody = featuresMatch[1];
|
||||
const nonBooleanKeys = featuresBody.split('\n')
|
||||
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/))
|
||||
.map(line => line.trim());
|
||||
|
||||
assert.strictEqual(nonBooleanKeys.length, 0, 'no non-boolean keys in a clean config');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,6 +363,37 @@ requirements-completed:
|
||||
assert.strictEqual(output.decisions, undefined, 'decisions excluded');
|
||||
});
|
||||
|
||||
test('extracts one-liner from body when not in frontmatter', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '01-01-SUMMARY.md'),
|
||||
`---
|
||||
phase: "01"
|
||||
key-files:
|
||||
- src/lib/db.ts
|
||||
---
|
||||
|
||||
# Phase 1: Foundation Summary
|
||||
|
||||
**JWT auth with refresh rotation using jose library**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 28 min
|
||||
- **Tasks:** 5
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.one_liner, 'JWT auth with refresh rotation using jose library',
|
||||
'one-liner should be extracted from body **bold** line');
|
||||
});
|
||||
|
||||
test('handles missing frontmatter fields gracefully', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
@@ -567,6 +598,98 @@ describe('todo complete command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// todo match-phase command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('todo match-phase command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
afterEach(() => cleanup(tmpDir));
|
||||
|
||||
test('returns empty matches when no todos exist', () => {
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.todo_count, 0);
|
||||
assert.deepStrictEqual(output.matches, []);
|
||||
});
|
||||
|
||||
test('matches todo by keyword overlap with phase name', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nNeed to handle token expiry for OAuth flows.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.todo_count, 1, 'should find 1 todo');
|
||||
assert.ok(output.matches.length > 0, 'should have matches');
|
||||
assert.strictEqual(output.matches[0].title, 'Add OAuth token refresh');
|
||||
assert.ok(output.matches[0].score > 0, 'score should be positive');
|
||||
assert.ok(output.matches[0].reasons.length > 0, 'should have reasons');
|
||||
});
|
||||
|
||||
test('does not match unrelated todo', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token expiry.');
|
||||
fs.writeFileSync(path.join(pendingDir, 'unrelated-todo.md'),
|
||||
'title: Fix CSS grid layout in dashboard\narea: ui\ncreated: 2026-03-01\n\nGrid columns break on mobile.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
const matchTitles = output.matches.map(m => m.title);
|
||||
assert.ok(matchTitles.includes('Add OAuth token refresh'), 'auth todo should match');
|
||||
assert.ok(!matchTitles.includes('Fix CSS grid layout in dashboard'), 'unrelated todo should not match');
|
||||
});
|
||||
|
||||
test('matches todo by area overlap', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token handling.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Auth System\n\n**Goal:** Build auth module\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
const authMatch = output.matches.find(m => m.title === 'Add OAuth token refresh');
|
||||
assert.ok(authMatch, 'should find auth todo');
|
||||
const hasAreaReason = authMatch.reasons.some(r => r.startsWith('area:'));
|
||||
assert.ok(hasAreaReason, 'should match on area');
|
||||
});
|
||||
|
||||
test('sorts matches by score descending', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'weak-match.md'),
|
||||
'title: Check token format\narea: general\ncreated: 2026-03-01\n\nToken format validation.');
|
||||
fs.writeFileSync(path.join(pendingDir, 'strong-match.md'),
|
||||
'title: Session management authentication OAuth token handling\narea: auth\ncreated: 2026-03-01\n\nSession auth OAuth tokens.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login, session handling, and token management\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.matches.length >= 2, 'should have multiple matches');
|
||||
for (let i = 1; i < output.matches.length; i++) {
|
||||
assert.ok(output.matches[i - 1].score >= output.matches[i].score,
|
||||
`match ${i-1} score (${output.matches[i-1].score}) should be >= match ${i} score (${output.matches[i].score})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// scaffold command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user