Merge branch 'main' into feat/materialize-new-project-config

This commit is contained in:
Diego Mariño
2026-03-18 23:12:42 +01:00
committed by GitHub
66 changed files with 3670 additions and 395 deletions

51
.release-monitor.sh Executable file
View File

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

View File

@@ -6,15 +6,44 @@ 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)
### 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 +1565,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- YOLO mode for autonomous execution
- Interactive mode with checkpoints
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.25.0...HEAD
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.26.0...HEAD
[1.26.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.26.0
[1.25.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.25.0
[1.24.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.24.0
[1.23.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.23.0

View File

@@ -342,19 +342,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 +498,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 +516,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 +541,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 +592,7 @@ Switch profiles:
/gsd:set-profile budget
```
Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`).
Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`).
Or configure via `/gsd:settings`.

View File

@@ -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);
}
@@ -694,6 +710,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 +741,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()}`;
@@ -1228,6 +1367,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 +1410,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 +1593,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 +1762,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 +1819,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 +1835,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 +1935,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 +1953,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 +1980,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 +1994,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 +2032,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 +2083,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 +2508,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 +2520,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 +2533,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);
@@ -2376,7 +2611,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 +2634,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, '..');
@@ -2421,6 +2659,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 +2727,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 +2801,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 +2836,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 +2857,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 +2944,36 @@ 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')) {
const featuresSection = '[features]\ncodex_hooks = true\n';
if (configContent.includes('[features]')) {
configContent = configContent.replace(/\[features\]\n/, featuresSection);
} else {
configContent = featuresSection + '\n' + configContent;
}
}
// 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 +2990,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 +3078,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 +3089,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 +3104,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 +3195,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 +3306,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,

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

@@ -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
@@ -358,7 +366,7 @@ Equivalent paths for other runtimes:
```
.planning/
├── PROJECT.md # Project vision, constraints, decisions
├── PROJECT.md # Project vision, constraints, decisions, evolution rules
├── REQUIREMENTS.md # Scoped requirements (v1/v2/out-of-scope)
├── ROADMAP.md # Phase breakdown with status tracking
├── STATE.md # Living memory: position, decisions, blockers, metrics

View File

@@ -331,7 +331,10 @@ node gsd-tools.cjs progress [json|table|bar]
node gsd-tools.cjs todo complete <filename>
# 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]

View File

@@ -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.

View File

@@ -133,6 +133,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
@@ -246,7 +248,7 @@ Valid override values: `opus`, `sonnet`, `haiku`, `inherit`
| `quality` | Opus for all decision-making, Sonnet for verification | Quota available, critical architecture work |
| `balanced` | Opus for planning only, Sonnet for everything else | Normal development (default) |
| `budget` | Sonnet for code-writing, Haiku for research/verification | High-volume work, less critical phases |
| `inherit` | All agents use current session model | Dynamic model switching (OpenCode `/model`) |
| `inherit` | All agents use current session model | Dynamic model switching, **non-Anthropic providers** (OpenRouter, local models) |
---

View File

@@ -20,33 +20,38 @@
- [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)
---
@@ -70,7 +75,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 +176,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 +226,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 +245,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 +273,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 +418,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 +468,7 @@
---
### 15. Plan Checking
### 16. Plan Checking
**Purpose:** Goal-backward verification that plans will achieve phase objectives before execution.
@@ -424,7 +480,7 @@
---
### 16. Post-Execution Verification
### 17. Post-Execution Verification
**Purpose:** Automated check that the codebase delivers what the phase promised.
@@ -436,7 +492,7 @@
---
### 17. Node Repair
### 18. Node Repair
**Purpose:** Autonomous recovery when task verification fails during execution.
@@ -450,7 +506,7 @@
---
### 18. Health Validation
### 19. Health Validation
**Command:** `/gsd:health [--repair]`
@@ -465,9 +521,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 +571,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 +627,7 @@
---
### 22. Model Profiles
### 26. Model Profiles
**Command:** `/gsd:set-profile <quality|balanced|budget|inherit>`
@@ -527,6 +638,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 +663,7 @@
## Brownfield Features
### 23. Codebase Mapping
### 27. Codebase Mapping
**Command:** `/gsd:map-codebase [area]`
@@ -579,7 +691,7 @@
## Utility Features
### 24. Debug System
### 28. Debug System
**Command:** `/gsd:debug [description]`
@@ -597,7 +709,7 @@
---
### 25. Todo Management
### 29. Todo Management
**Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos`
@@ -611,7 +723,7 @@
---
### 26. Statistics Dashboard
### 30. Statistics Dashboard
**Command:** `/gsd:stats`
@@ -625,7 +737,7 @@
---
### 27. Update System
### 31. Update System
**Command:** `/gsd:update`
@@ -640,7 +752,7 @@
---
### 28. Settings Management
### 32. Settings Management
**Command:** `/gsd:settings`
@@ -673,7 +785,7 @@
---
### 29. Test Generation
### 33. Test Generation
**Command:** `/gsd:add-tests [N]`
@@ -688,7 +800,7 @@
## Infrastructure Features
### 30. Git Integration
### 34. Git Integration
**Purpose:** Atomic commits, branching strategies, and clean history management.
@@ -714,7 +826,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 +841,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 +864,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 +884,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 +920,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.

View File

@@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic
│ │ /gsd:verify-work │ │ <- Manual UAT
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼─────────┐ │
│ │ /gsd:ship │ │ <- Create PR (optional)
│ └──────────┬─────────┘ │
│ │ │
│ Next Phase?────────────┘
│ │ No
└─────────────┼──────────────┘
@@ -284,6 +288,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
| `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase |
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves | After planning is complete |
| `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes |
| `/gsd:ship [N]` | Create PR from verified work | After verification passes |
| `/gsd:next` | Auto-detect state and run next step | Anytime — "what should I do next?" |
| `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) |
| `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone |
| `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified |
@@ -295,7 +301,8 @@ Controlled by `workflow.ui_safety_gate` config toggle.
|---------|---------|-------------|
| `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" |
| `/gsd:resume-work` | Restore full context from last session | Starting a new session |
| `/gsd:pause-work` | Save context handoff | Stopping mid-phase |
| `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase |
| `/gsd:session-report` | Generate session summary with work and outcomes | End of session, stakeholder sharing |
| `/gsd:help` | Show all commands | Quick reference |
| `/gsd:update` | Update GSD with changelog preview | Check for new versions |
| `/gsd:join-discord` | Open Discord community invite | Questions or community |
@@ -363,7 +370,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
@@ -407,25 +414,25 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
### 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 +449,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 +548,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 +564,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 +594,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 +612,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

View File

@@ -15,9 +15,11 @@
* 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
* 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)
@@ -255,6 +257,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 +290,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 +299,7 @@ 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;
}
@@ -522,8 +540,10 @@ async function main() {
const subcommand = args[1];
if (subcommand === 'complete') {
commands.cmdTodoComplete(cwd, args[2], raw);
} else if (subcommand === 'match-phase') {
commands.cmdTodoMatchPhase(cwd, args[2], raw);
} else {
error('Unknown todo subcommand. Available: complete');
error('Unknown todo subcommand. Available: complete, match-phase');
}
break;
}

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, 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());
@@ -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');
}
@@ -241,8 +241,9 @@ function cmdCommit(cwd, message, files, raw, amend) {
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')) {
@@ -412,7 +413,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 +449,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');
@@ -547,7 +672,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 +684,7 @@ function cmdStats(cwd, format, raw) {
status: 'Not Started',
});
}
} catch {}
} catch { /* intentionally empty */ }
try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
@@ -595,7 +720,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 +738,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 +751,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;
@@ -704,6 +829,7 @@ module.exports = {
cmdWebsearch,
cmdProgressRender,
cmdTodoComplete,
cmdTodoMatchPhase,
cmdScaffold,
cmdStats,
};

View File

@@ -71,7 +71,7 @@ function buildNewProjectConfig(userChoices) {
delete userDefaults.depth;
try {
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
} catch {}
} catch { /* intentionally empty */ }
}
}
} catch {

View File

@@ -64,6 +64,8 @@ function loadConfig(cwd) {
nyquist_validation: true,
parallelization: true,
brave_search: false,
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
};
try {
@@ -75,7 +77,7 @@ 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 {}
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ }
}
const get = (key, nested) => {
@@ -106,6 +108,8 @@ function loadConfig(cwd) {
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
parallelization,
brave_search: get('brave_search') ?? defaults.brave_search,
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
context_window: get('context_window') ?? defaults.context_window,
model_overrides: parsed.model_overrides || null,
};
} catch {
@@ -248,6 +252,27 @@ function execGit(cwd, args) {
};
}
// ─── Common path helpers ──────────────────────────────────────────────────────
/** 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) {
@@ -368,7 +393,7 @@ function findPhaseInternal(cwd, phase) {
return result;
}
}
} catch {}
} catch { /* intentionally empty */ }
return null;
}
@@ -403,7 +428,7 @@ function getArchivedPhaseDirs(cwd) {
});
}
}
} catch {}
} catch { /* intentionally empty */ }
return results;
}
@@ -420,6 +445,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,7 +554,7 @@ 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());
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
const headerMatch = content.match(phasePattern);
@@ -472,6 +582,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 +609,15 @@ 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;
}
// ─── Misc utilities ───────────────────────────────────────────────────────────
@@ -549,13 +680,13 @@ 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 roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd);
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\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;
@@ -597,6 +728,10 @@ module.exports = {
getMilestoneInfo,
getMilestonePhaseFilter,
stripShippedMilestones,
extractCurrentMilestone,
replaceInCurrentMilestone,
toPosixPath,
MODEL_ALIAS_MAP,
planningDir,
planningPaths,
};

View File

@@ -5,7 +5,7 @@
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 cmdInitExecutePhase(cwd, phase, raw) {
if (!phase) {
@@ -31,6 +31,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
// Config flags
commit_docs: config.commit_docs,
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,
@@ -153,7 +154,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
if (uatFile) {
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
}
} catch {}
} catch { /* intentionally empty */ }
}
output(result, raw);
@@ -167,17 +168,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') ||
@@ -307,7 +317,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
@@ -459,7 +469,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
if (uatFile) {
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
}
} catch {}
} catch { /* intentionally empty */ }
}
output(result, raw);
@@ -494,9 +504,9 @@ function cmdInitTodos(cwd, area, raw) {
area: todoArea,
path: '.planning/todos/pending/' + file,
});
} catch {}
} catch { /* intentionally empty */ }
}
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Config
@@ -543,9 +553,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 +564,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
@@ -593,7 +603,7 @@ function cmdInitMapCodebase(cwd, raw) {
let existingMaps = [];
try {
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
} catch {}
} catch { /* intentionally empty */ }
const result = {
// Models
@@ -633,8 +643,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 +652,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 +705,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 +736,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

View File

@@ -137,10 +137,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
// Count tasks
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
totalTasks += taskMatches.length;
} catch {}
} catch { /* intentionally empty */ }
}
}
} catch {}
} catch { /* intentionally empty */ }
// Archive ROADMAP.md
if (fs.existsSync(roadmapPath)) {
@@ -220,7 +220,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
archivedCount++;
}
phasesArchived = archivedCount > 0;
} catch {}
} catch { /* intentionally empty */ }
}
const result = {

View File

@@ -4,7 +4,7 @@
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, 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');
@@ -319,7 +319,7 @@ function cmdPhaseAdd(cwd, description, raw) {
}
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)
@@ -376,7 +376,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 +401,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 +470,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 +538,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
} else {
// Integer removal: renumber all subsequent integer phases
@@ -598,7 +598,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
}
// Update ROADMAP.md
@@ -760,7 +760,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 +818,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,7 +835,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
break;
}
}
} catch {}
} catch { /* intentionally empty */ }
}
// Update STATE.md
@@ -880,6 +880,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
);
// Increment Completed Phases counter (#956)
const completedMatch = stateContent.match(/\*\*Completed Phases:\*\*\s*(\d+)/);
if (completedMatch) {
const newCompleted = parseInt(completedMatch[1], 10) + 1;
stateContent = stateContent.replace(
/(\*\*Completed Phases:\*\*\s*)\d+/,
`$1${newCompleted}`
);
// Recalculate percent based on completed / total (#956)
const totalMatch = stateContent.match(/\*\*Total Phases:\*\*\s*(\d+)/);
if (totalMatch) {
const totalPhases = parseInt(totalMatch[1], 10);
if (totalPhases > 0) {
const newPercent = Math.round((newCompleted / totalPhases) * 100);
stateContent = stateContent.replace(
/(\*\*Progress:\*\*\s*)\d+%/,
`$1${newPercent}%`
);
// Also update percent field if it exists separately
stateContent = stateContent.replace(
/(percent:\s*)\d+/,
`$1${newPercent}`
);
}
}
}
writeStateMd(statePath, stateContent, cwd);
}

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, replaceInCurrentMilestone } = require('./core.cjs');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
@@ -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);
@@ -99,7 +99,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const phasesDir = path.join(cwd, '.planning', 'phases');
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
@@ -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');

View File

@@ -4,9 +4,14 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs');
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
/** Shorthand — every state command needs this path */
function getStatePath(cwd) {
return planningPaths(cwd).state;
}
// Shared helper: extract a field value from STATE.md content.
// Supports both **Field:** bold and plain Field: format.
function stateExtractField(content, fieldName) {
@@ -26,7 +31,7 @@ function cmdStateLoad(cwd, raw) {
let stateRaw = '';
try {
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
} catch {}
} catch { /* intentionally empty */ }
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
@@ -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');
@@ -125,7 +130,7 @@ function cmdStatePatch(cwd, patches, raw) {
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');
@@ -159,7 +164,7 @@ function cmdStateUpdate(cwd, field, value) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
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)) {
@@ -576,7 +570,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;
@@ -611,7 +605,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
totalPlans = diskTotalPlans;
completedPlans = diskTotalSummaries;
}
} catch {}
} catch { /* intentionally empty */ }
}
let progressPercent = null;
@@ -677,10 +671,54 @@ 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) {
@@ -778,6 +816,53 @@ 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,
@@ -796,4 +881,6 @@ module.exports = {
cmdStateSnapshot,
cmdStateJson,
cmdStateBeginPhase,
cmdSignalWaiting,
cmdSignalResume,
};

View File

@@ -5,7 +5,7 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, output, error } = require('./core.cjs');
const { safeReadFile, 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) {
@@ -490,7 +490,7 @@ function cmdValidateConsistency(cwd, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// Check: frontmatter in plans has required fields
try {
@@ -510,7 +510,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,7 +599,7 @@ 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');
@@ -641,7 +641,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 +652,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 +671,7 @@ function cmdValidateHealth(cwd, options, raw) {
}
}
}
} catch {}
} catch { /* intentionally empty */ }
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
try {
@@ -689,13 +689,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 +712,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) {

View File

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

View File

@@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p
Each task gets its own commit immediately after completion.
> **Parallel agents:** When running as a parallel executor (spawned by execute-phase),
> use `--no-verify` on all commits to avoid pre-commit hook lock contention.
> The orchestrator validates hooks once after all agents complete.
```
{type}({phase}-{plan}): {task-name}

View File

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

View File

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

View File

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

View File

@@ -242,6 +242,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.
@@ -406,6 +447,30 @@ For each selected area, conduct a focused discussion loop.
**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
@@ -544,6 +609,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 +665,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>

View File

@@ -6,10 +6,42 @@ 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:** Uses `@gsd-executor` agent reference — if subagent spawning hangs or fails to return,
fall back to **sequential inline execution**: read and follow execute-plan.md directly for each plan
instead of spawning parallel agents. This is slower but reliable.
- **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.
</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">
@@ -37,6 +69,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 +191,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 +205,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 +223,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 +250,17 @@ 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:**
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 +435,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.
@@ -412,6 +580,28 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): co
```
</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 +655,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 +665,20 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
/gsd:plan-phase {next} — plan next phase
/gsd:execute-phase {next} — execute next phase
```
Only suggest the commands listed above. Do not invent or hallucinate command names.
</step>
</process>
<context_efficiency>
Orchestrator: ~10-15% context. Subagents: fresh 200k each. No polling (Task blocks). No context bleed.
Orchestrator: ~10-15% context for 200k windows, can use more for 1M+ windows.
Subagents: fresh context each (200k-1M depending on model). No polling (Task blocks). No context bleed.
For 1M+ context models, consider:
- Passing richer context (code snippets, dependency outputs) directly to executors instead of just file paths
- Running small phases (≤3 plans, no dependencies) inline without subagent spawning overhead
- Relaxing /clear recommendations — context rot onset is much further out with 5x window
</context_efficiency>
<failure_handling>

View File

@@ -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>
@@ -370,7 +373,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not "
Include: duration, start/end times, task count, file count.
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition".
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step".
</step>
<step name="update_current_position">

View File

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

View File

@@ -157,3 +157,25 @@ Report final status.
- Orphaned plan cleanup
</repair_actions>
<stale_task_cleanup>
**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze.
These are left behind when subagents are force-killed and consume disk space.
When `--repair` is active, detect and clean up:
```bash
# Check for stale task directories (older than 24 hours)
TASKS_DIR="$HOME/.claude/tasks"
if [ -d "$TASKS_DIR" ]; then
STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l)
if [ "$STALE_COUNT" -gt 0 ]; then
echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/"
echo " These are leftover from crashed subagent sessions."
echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)"
fi
fi
```
Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)`
</stale_task_cleanup>

View File

@@ -151,6 +151,21 @@ Usage: `/gsd:quick`
Usage: `/gsd:quick --research --full`
Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md`
---
**`/gsd:fast [description]`**
Execute a trivial task inline — no subagents, no planning files, no overhead.
For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md.
- No PLAN.md or SUMMARY.md created
- No subagent spawned (runs inline)
- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial
- Atomic commit with conventional message
Usage: `/gsd:fast "fix the typo in README"`
Usage: `/gsd:fast "add .env to gitignore"`
### Roadmap Management
**`/gsd:add-phase <description>`**
@@ -308,6 +323,20 @@ 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`
### Milestone Auditing
**`/gsd:audit-milestone [version]`**

View File

@@ -82,12 +82,25 @@ mkdir -p .planning/codebase
Continue to spawn_agents.
</step>
<step name="spawn_agents">
<step name="detect_runtime_capabilities">
Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation.
**Runtimes with Task tool:** Claude Code, Cursor (native subagent support)
**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others
**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis):
→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead.
**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context.
</step>
<step name="spawn_agents" condition="Task tool is available">
Spawn 4 parallel gsd-codebase-mapper agents.
Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly.
**Agent 1: Tech Focus**
@@ -195,6 +208,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 +351,10 @@ End workflow.
<success_criteria>
- .planning/codebase/ directory created
- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- Agents write documents directly (orchestrator doesn't receive document contents)
- Read agent output files to collect confirmations
- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent)
- All 7 codebase documents exist
- No empty documents (each should have >20 lines)
- Clear completion summary with line counts
- User offered clear next steps in GSD style
</success_criteria>

View File

@@ -54,6 +54,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

View File

@@ -323,6 +323,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:**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ AskUserQuestion([
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" },
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
{ label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" }
{ label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" }
]
},
{
@@ -160,7 +160,8 @@ Merge new settings into existing config.json:
"branching_strategy": "none" | "phase" | "milestone"
},
"hooks": {
"context_warnings": true/false
"context_warnings": true/false,
"workflow_guard": true/false
}
}
```

View File

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

View File

@@ -1,3 +1,19 @@
<internal_workflow>
**This is an INTERNAL workflow — NOT a user-facing command.**
There is no `/gsd:transition` command. This workflow is invoked automatically by
`execute-phase` during auto-advance, or inline by the orchestrator after phase
verification. Users should never be told to run `/gsd:transition`.
**Valid user commands for phase progression:**
- `/gsd:discuss-phase {N}` — discuss a phase before planning
- `/gsd:plan-phase {N}` — plan a phase
- `/gsd:execute-phase {N}` — execute a phase
- `/gsd:progress` — see roadmap progress
</internal_workflow>
<required_reading>
**Read these files NOW:**

View File

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

View File

@@ -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.endsWith('.js'));
for (const hookFile of hookFiles) {
try {
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (hookVersion !== installed && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
// No version header at all — definitely stale (pre-version-tracking)
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
}
} catch (e) {}
}
}
} catch (e) {}
}
let latest = null;
try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
@@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', `
update_available: latest && installed !== latest,
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000)
checked: Math.floor(Date.now() / 1000),
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
};
fs.writeFileSync(cacheFile, JSON.stringify(result));

View File

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

View File

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

View File

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

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
"bin": {
"get-shit-done-cc": "bin/install.js"

View File

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

View File

@@ -567,6 +567,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
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => {
// Count gsd-* directories — should be 31
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
assert.strictEqual(dirs.length, 39, `expected 39 skill folders, got ${dirs.length}`);
assert.strictEqual(dirs.length, 43, `expected 43 skill folders, got ${dirs.length}`);
} finally {
fs.rmSync(tempDir, { recursive: true });
}
@@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process');
const crypto = require('crypto');
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
const EXPECTED_SKILLS = 39;
const EXPECTED_SKILLS = 43;
const EXPECTED_AGENTS = 16;
function runCopilotInstall(cwd) {

View File

@@ -10,6 +10,7 @@ const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { createTempProject, cleanup } = require('./helpers.cjs');
const {
loadConfig,
@@ -35,14 +36,13 @@ describe('loadConfig', () => {
let originalCwd;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
originalCwd = process.cwd();
});
afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
function writeConfig(obj) {
@@ -129,12 +129,11 @@ describe('resolveModelInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
function writeConfig(obj) {
@@ -276,62 +275,11 @@ describe('generateSlugInternal', () => {
});
});
// ─── normalizePhaseName ────────────────────────────────────────────────────────
describe('normalizePhaseName', () => {
test('pads single digit', () => {
assert.strictEqual(normalizePhaseName('1'), '01');
});
test('preserves double digit', () => {
assert.strictEqual(normalizePhaseName('12'), '12');
});
test('handles letter suffix', () => {
assert.strictEqual(normalizePhaseName('1A'), '01A');
});
test('handles decimal phases', () => {
assert.strictEqual(normalizePhaseName('2.1'), '02.1');
});
test('handles multi-level decimals', () => {
assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3');
});
test('returns non-matching input unchanged', () => {
assert.strictEqual(normalizePhaseName('abc'), 'abc');
});
});
// ─── comparePhaseNum ───────────────────────────────────────────────────────────
describe('comparePhaseNum', () => {
test('sorts integer phases numerically', () => {
assert.ok(comparePhaseNum('1', '2') < 0);
assert.ok(comparePhaseNum('10', '2') > 0);
});
test('sorts letter suffixes', () => {
assert.ok(comparePhaseNum('12', '12A') < 0);
assert.ok(comparePhaseNum('12A', '12B') < 0);
});
test('sorts decimal phases', () => {
assert.ok(comparePhaseNum('2', '2.1') < 0);
assert.ok(comparePhaseNum('2.1', '2.2') < 0);
});
test('handles multi-level decimals', () => {
assert.ok(comparePhaseNum('1.1', '1.1.2') < 0);
assert.ok(comparePhaseNum('1.1.2', '1.2') < 0);
});
test('returns 0 for equal phases', () => {
assert.strictEqual(comparePhaseNum('1', '1'), 0);
assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0);
});
});
// ─── normalizePhaseName / comparePhaseNum ──────────────────────────────────────
// NOTE: Comprehensive tests for normalizePhaseName and comparePhaseNum are in
// phase.test.cjs (which covers all edge cases: hybrid, letter-suffix,
// multi-level decimal, case-insensitive, directory-slug, and full sort order).
// Removed duplicates here to keep a single authoritative test location.
// ─── safeReadFile ──────────────────────────────────────────────────────────────
@@ -343,7 +291,7 @@ describe('safeReadFile', () => {
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('reads existing file', () => {
@@ -363,12 +311,11 @@ describe('pathExistsInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('returns true for existing path', () => {
@@ -390,12 +337,11 @@ describe('getMilestoneInfo', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('extracts version and name from roadmap', () => {
@@ -502,7 +448,7 @@ describe('searchPhaseInDir', () => {
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('finds phase directory by normalized prefix', () => {
@@ -564,12 +510,11 @@ describe('findPhaseInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('finds phase in current phases directory', () => {
@@ -605,12 +550,11 @@ describe('getRoadmapPhaseInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
// Bug: getRoadmapPhaseInternal was missing from module.exports
@@ -687,12 +631,11 @@ describe('getMilestonePhaseFilter', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('filters directories to only current milestone phases', () => {

View File

@@ -0,0 +1,81 @@
/**
* Cursor conversion regression tests.
*
* Ensures Cursor frontmatter names are emitted as plain identifiers
* (without surrounding quotes), so Cursor does not treat quotes as
* literal parts of skill/subagent names.
*/
process.env.GSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert');
const {
convertClaudeCommandToCursorSkill,
convertClaudeAgentToCursorAgent,
} = require('../bin/install.js');
describe('convertClaudeCommandToCursorSkill', () => {
test('writes unquoted Cursor skill name in frontmatter', () => {
const input = `---
name: quick
description: Execute a quick task
---
<objective>
Test body
</objective>
`;
const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick');
const nameMatch = result.match(/^name:\s*(.+)$/m);
assert.ok(nameMatch, 'frontmatter contains name field');
assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar');
assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted');
});
test('preserves slash for slash commands in markdown body', () => {
const input = `---
name: gsd:plan-phase
description: Plan a phase
---
Next:
/gsd:execute-phase 17
/gsd-help
gsd:progress
`;
const result = convertClaudeCommandToCursorSkill(input, 'gsd-plan-phase');
assert.ok(result.includes('/gsd-execute-phase 17'), 'slash command remains slash-prefixed');
assert.ok(result.includes('/gsd-help'), 'existing slash command is preserved');
assert.ok(result.includes('gsd-progress'), 'non-slash gsd: references still normalize');
assert.ok(!result.includes('/gsd:execute-phase'), 'legacy colon command form is removed');
});
});
describe('convertClaudeAgentToCursorAgent', () => {
test('writes unquoted Cursor agent name in frontmatter', () => {
const input = `---
name: gsd-planner
description: Planner agent
tools: Read, Write
color: green
---
<role>
Planner body
</role>
`;
const result = convertClaudeAgentToCursorAgent(input);
const nameMatch = result.match(/^name:\s*(.+)$/m);
assert.ok(nameMatch, 'frontmatter contains name field');
assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar');
assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted');
});
});

View File

@@ -1,47 +0,0 @@
/**
* GSD Tools Tests - Gemini agent conversion
*
* Verifies Gemini-specific agent frontmatter conversion removes
* unsupported fields while preserving converted tools and body text.
*/
process.env.GSD_TEST_MODE = '1';
const { test, describe } = require('node:test');
const assert = require('node:assert');
const { convertClaudeToGeminiAgent } = require('../bin/install.js');
describe('convertClaudeToGeminiAgent', () => {
test('drops unsupported skills frontmatter while keeping converted tools', () => {
const input = `---
name: gsd-codebase-mapper
description: Explores codebase and writes structured analysis documents.
tools: Read, Bash, Grep, Glob, Write
color: cyan
skills:
- gsd-mapper-workflow
---
<role>
Use \${PHASE} in shell examples.
</role>`;
const result = convertClaudeToGeminiAgent(input);
const frontmatter = result.split('---')[1] || '';
assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name');
assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description');
assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array');
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command');
assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content');
assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob');
assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file');
assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field');
assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field');
assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items');
assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini');
assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern');
});
});

View File

@@ -0,0 +1,134 @@
/**
* Model Profiles Tests
*
* Tests for MODEL_PROFILES data structure, VALID_PROFILES list,
* formatAgentToModelMapAsTable, and getAgentToModelMapForProfile.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const {
MODEL_PROFILES,
VALID_PROFILES,
formatAgentToModelMapAsTable,
getAgentToModelMapForProfile,
} = require('../get-shit-done/bin/lib/model-profiles.cjs');
// ─── MODEL_PROFILES data integrity ────────────────────────────────────────────
describe('MODEL_PROFILES', () => {
test('contains all expected GSD agents', () => {
const expectedAgents = [
'gsd-planner', 'gsd-roadmapper', 'gsd-executor',
'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer',
'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier',
'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor',
'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor',
];
for (const agent of expectedAgents) {
assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`);
}
});
test('every agent has quality, balanced, and budget profiles', () => {
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
assert.ok(profiles.quality, `${agent} missing quality profile`);
assert.ok(profiles.balanced, `${agent} missing balanced profile`);
assert.ok(profiles.budget, `${agent} missing budget profile`);
}
});
test('all profile values are valid model aliases', () => {
const validModels = ['opus', 'sonnet', 'haiku'];
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
for (const [profile, model] of Object.entries(profiles)) {
assert.ok(
validModels.includes(model),
`${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}`
);
}
}
});
test('quality profile never uses haiku', () => {
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
assert.notStrictEqual(
profiles.quality, 'haiku',
`${agent} quality profile should not use haiku`
);
}
});
});
// ─── VALID_PROFILES ───────────────────────────────────────────────────────────
describe('VALID_PROFILES', () => {
test('contains quality, balanced, and budget', () => {
assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']);
});
test('is derived from MODEL_PROFILES keys', () => {
const fromData = Object.keys(MODEL_PROFILES['gsd-planner']);
assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort());
});
});
// ─── getAgentToModelMapForProfile ─────────────────────────────────────────────
describe('getAgentToModelMapForProfile', () => {
test('returns correct models for balanced profile', () => {
const map = getAgentToModelMapForProfile('balanced');
assert.strictEqual(map['gsd-planner'], 'opus');
assert.strictEqual(map['gsd-codebase-mapper'], 'haiku');
assert.strictEqual(map['gsd-verifier'], 'sonnet');
});
test('returns correct models for budget profile', () => {
const map = getAgentToModelMapForProfile('budget');
assert.strictEqual(map['gsd-planner'], 'sonnet');
assert.strictEqual(map['gsd-phase-researcher'], 'haiku');
});
test('returns correct models for quality profile', () => {
const map = getAgentToModelMapForProfile('quality');
assert.strictEqual(map['gsd-planner'], 'opus');
assert.strictEqual(map['gsd-executor'], 'opus');
});
test('returns all agents in the map', () => {
const map = getAgentToModelMapForProfile('balanced');
const agentCount = Object.keys(MODEL_PROFILES).length;
assert.strictEqual(Object.keys(map).length, agentCount);
});
});
// ─── formatAgentToModelMapAsTable ─────────────────────────────────────────────
describe('formatAgentToModelMapAsTable', () => {
test('produces a table with header and separator', () => {
const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' };
const table = formatAgentToModelMapAsTable(map);
assert.ok(table.includes('Agent'), 'should have Agent header');
assert.ok(table.includes('Model'), 'should have Model header');
assert.ok(table.includes('─'), 'should have separator line');
assert.ok(table.includes('gsd-planner'), 'should list agent');
assert.ok(table.includes('opus'), 'should list model');
});
test('pads columns correctly', () => {
const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' };
const table = formatAgentToModelMapAsTable(map);
const lines = table.split('\n').filter(l => l.trim());
// Separator line uses ┼, data/header lines use │
const dataLines = lines.filter(l => l.includes('│'));
const pipePositions = dataLines.map(l => l.indexOf('│'));
const unique = [...new Set(pipePositions)];
assert.strictEqual(unique.length, 1, 'all data lines should align on │');
});
test('handles empty map', () => {
const table = formatAgentToModelMapAsTable({});
assert.ok(table.includes('Agent'), 'should still have header');
});
});

View File

@@ -0,0 +1,197 @@
/**
* Profile Output Tests
*
* Tests for profile rendering commands and PROFILING_QUESTIONS data.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs');
const {
PROFILING_QUESTIONS,
CLAUDE_INSTRUCTIONS,
} = require('../get-shit-done/bin/lib/profile-output.cjs');
// ─── PROFILING_QUESTIONS data ─────────────────────────────────────────────────
describe('PROFILING_QUESTIONS', () => {
test('is a non-empty array', () => {
assert.ok(Array.isArray(PROFILING_QUESTIONS));
assert.ok(PROFILING_QUESTIONS.length > 0);
});
test('each question has required fields', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(q.dimension, `question missing dimension`);
assert.ok(q.header, `${q.dimension} missing header`);
assert.ok(q.question, `${q.dimension} missing question`);
assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`);
assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`);
}
});
test('each option has label, value, and rating', () => {
for (const q of PROFILING_QUESTIONS) {
for (const opt of q.options) {
assert.ok(opt.label, `${q.dimension} option missing label`);
assert.ok(opt.value, `${q.dimension} option missing value`);
assert.ok(opt.rating, `${q.dimension} option missing rating`);
}
}
});
test('all dimension keys are unique', () => {
const dims = PROFILING_QUESTIONS.map(q => q.dimension);
const unique = [...new Set(dims)];
assert.strictEqual(dims.length, unique.length);
});
});
// ─── CLAUDE_INSTRUCTIONS ──────────────────────────────────────────────────────
describe('CLAUDE_INSTRUCTIONS', () => {
test('is a non-empty object', () => {
assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object');
assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0);
});
test('each dimension has at least one instruction', () => {
for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) {
assert.ok(typeof instructions === 'object', `${dim} should be an object`);
assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`);
}
});
test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(
CLAUDE_INSTRUCTIONS[q.dimension],
`${q.dimension} has questions but no CLAUDE_INSTRUCTIONS`
);
}
});
});
// ─── write-profile command ────────────────────────────────────────────────────
describe('write-profile command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('writes USER-PROFILE.md from analysis JSON', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
explanation_depth: { rating: 'concise', confidence: 'HIGH' },
debugging_approach: { rating: 'fix-first', confidence: 'LOW' },
ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' },
vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' },
frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' },
learning_style: { rating: 'hands-on', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.profile_path, 'should return profile_path');
assert.ok(out.dimensions_scored > 0, 'should have scored dimensions');
});
test('errors when --input is missing', () => {
const result = runGsdTools('write-profile --raw', tmpDir);
assert.ok(!result.success, 'should fail without --input');
assert.ok(result.error.includes('--input'), 'should mention --input');
});
});
// ─── generate-claude-md command ───────────────────────────────────────────────
describe('generate-claude-md command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject();
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('generates CLAUDE.md with --auto flag', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
if (fs.existsSync(outputPath)) {
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.length > 0, 'should have content');
}
});
test('does not overwrite existing CLAUDE.md without --force', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n');
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
// Should merge, not overwrite
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.length > 0, 'should still have content');
});
});
// ─── generate-dev-preferences ─────────────────────────────────────────────────
describe('generate-dev-preferences command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('errors when --analysis is missing', () => {
const result = runGsdTools('generate-dev-preferences --raw', tmpDir);
assert.ok(!result.success, 'should fail without --analysis');
assert.ok(result.error.includes('--analysis'), 'should mention --analysis');
});
test('generates preferences from analysis file', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.command_path || out.command_name, 'should return command output');
});
});

View File

@@ -0,0 +1,160 @@
/**
* Profile Pipeline Tests
*
* Tests for session scanning, message extraction, and profile sampling.
* Uses synthetic session data in temp directories via --path override.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── scan-sessions ────────────────────────────────────────────────────────────
describe('scan-sessions command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns empty array for empty sessions directory', () => {
const sessionsDir = path.join(tmpDir, 'projects');
fs.mkdirSync(sessionsDir, { recursive: true });
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(Array.isArray(out), 'should return an array');
assert.strictEqual(out.length, 0, 'should be empty');
});
test('scans synthetic project directory', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'test-project-abc123');
fs.mkdirSync(projectDir, { recursive: true });
// Create a synthetic session file
const sessionData = [
JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }),
JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }),
].join('\n');
fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData);
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(Array.isArray(out), 'should return array');
assert.strictEqual(out.length, 1, 'should find 1 project');
assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session');
});
test('reports multiple sessions and sizes', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'multi-session-project');
fs.mkdirSync(projectDir, { recursive: true });
for (let i = 1; i <= 3; i++) {
const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() });
fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n');
}
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out[0].sessionCount, 3);
assert.ok(out[0].totalSize > 0, 'should have non-zero size');
});
});
// ─── extract-messages ─────────────────────────────────────────────────────────
describe('extract-messages command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
});
afterEach(() => {
cleanup(tmpDir);
});
test('extracts user messages from synthetic session', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'my-project');
fs.mkdirSync(projectDir, { recursive: true });
const messages = [
{ type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() },
{ type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() },
{ type: 'user', userType: 'internal', isMeta: true, message: { content: '<local-command' }, timestamp: Date.now() },
];
fs.writeFileSync(
path.join(projectDir, 'session-001.jsonl'),
messages.map(m => JSON.stringify(m)).join('\n')
);
const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages');
assert.strictEqual(out.project, 'my-project');
assert.ok(out.output_file, 'should have output file path');
});
test('filters out meta and internal messages', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'filter-test');
fs.mkdirSync(projectDir, { recursive: true });
const messages = [
{ type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() },
{ type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: '<local-command test' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: '' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: 'second real' }, timestamp: Date.now() },
];
fs.writeFileSync(
path.join(projectDir, 'session-001.jsonl'),
messages.map(m => JSON.stringify(m)).join('\n')
);
const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages');
});
});
// ─── profile-questionnaire ────────────────────────────────────────────────────
describe('profile-questionnaire command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns questionnaire structure', () => {
const result = runGsdTools('profile-questionnaire --raw', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.questions, 'should have questions array');
assert.ok(out.questions.length > 0, 'should have at least one question');
assert.ok(out.questions[0].dimension, 'each question should have a dimension');
assert.ok(out.questions[0].options, 'each question should have options');
});
});

View File

@@ -1,19 +1,23 @@
/**
* OpenCode Agent Frontmatter Conversion Tests
* Runtime Converter Tests — OpenCode + Gemini
*
* Validates that convertClaudeToOpencodeFrontmatter correctly converts
* agent frontmatter for OpenCode compatibility when isAgent: true.
* Tests for small runtime-specific conversion functions from install.js.
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
*
* Bug: Without isAgent flag, the function strips name: (agents need it),
* keeps color:/skills:/tools: record (should strip), and doesn't add
* model: inherit / mode: subagent (required by OpenCode agents).
* OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes)
* model: inherit is NOT added (OpenCode doesn't support it — see #1156)
* but mode: subagent IS added (required by OpenCode agents).
* Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping)
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
process.env.GSD_TEST_MODE = '1';
const { convertClaudeToOpencodeFrontmatter } = require('../bin/install.js');
const {
convertClaudeToOpencodeFrontmatter,
convertClaudeToGeminiAgent,
} = require('../bin/install.js');
// Sample Claude agent frontmatter (matches actual GSD agent format)
const SAMPLE_AGENT = `---
@@ -54,10 +58,10 @@ describe('OpenCode agent conversion (isAgent: true)', () => {
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
});
test('adds model: inherit', () => {
test('does not add model: inherit (OpenCode does not support it)', () => {
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
const frontmatter = result.split('---')[1];
assert.ok(frontmatter.includes('model: inherit'), 'model: inherit should be added');
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError');
});
test('adds mode: subagent', () => {
@@ -141,3 +145,41 @@ describe('OpenCode command conversion (isAgent: false, default)', () => {
assert.ok(frontmatter.includes('description:'), 'description should be kept');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Gemini CLI agent conversion (merged from gemini-config.test.cjs)
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeToGeminiAgent', () => {
test('drops unsupported skills frontmatter while keeping converted tools', () => {
const input = `---
name: gsd-codebase-mapper
description: Explores codebase and writes structured analysis documents.
tools: Read, Bash, Grep, Glob, Write
color: cyan
skills:
- gsd-mapper-workflow
---
<role>
Use \${PHASE} in shell examples.
</role>`;
const result = convertClaudeToGeminiAgent(input);
const frontmatter = result.split('---')[1] || '';
assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name');
assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description');
assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array');
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command');
assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content');
assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob');
assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file');
assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field');
assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field');
assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items');
assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini');
assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern');
});
});

186
tests/template.test.cjs Normal file
View File

@@ -0,0 +1,186 @@
/**
* Template Tests
*
* Tests for cmdTemplateSelect (heuristic template selection) and
* cmdTemplateFill (summary, plan, verification template generation).
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── template select ──────────────────────────────────────────────────────────
describe('template select command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create a phase directory with a plan
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('selects minimal template for simple plan', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
fs.writeFileSync(planPath, [
'# Plan',
'',
'### Task 1',
'Do the thing.',
'',
'File: `src/index.ts`',
].join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'minimal');
assert.ok(out.template.includes('summary-minimal'));
});
test('selects standard template for moderate plan', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
fs.writeFileSync(planPath, [
'# Plan',
'',
'### Task 1',
'Create `src/auth/login.ts`',
'',
'### Task 2',
'Create `src/auth/register.ts`',
'',
'### Task 3',
'Update `src/routes/index.ts`',
'',
'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`',
].join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'standard');
});
test('selects complex template for plan with decisions and many files', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
const lines = ['# Plan', ''];
for (let i = 1; i <= 6; i++) {
lines.push(`### Task ${i}`, `Do task ${i}.`, '');
}
lines.push('Made a decision about architecture.', 'Another decision here.');
for (let i = 1; i <= 8; i++) {
lines.push(`File: \`src/module${i}/index.ts\``);
}
fs.writeFileSync(planPath, lines.join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'complex');
});
test('returns standard as fallback for nonexistent file', () => {
const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'standard');
assert.ok(out.error, 'should include error message');
});
});
// ─── template fill ────────────────────────────────────────────────────────────
describe('template fill command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('fills summary template', () => {
const result = runGsdTools('template fill summary --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-01-SUMMARY.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('---'), 'should have frontmatter');
assert.ok(content.includes('Phase 1'), 'should reference phase');
assert.ok(content.includes('Accomplishments'), 'should have accomplishments section');
});
test('fills plan template', () => {
const result = runGsdTools('template fill plan --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-01-PLAN.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('---'), 'should have frontmatter');
assert.ok(content.includes('Objective'), 'should have objective section');
assert.ok(content.includes('<task type="code">'), 'should have task XML');
});
test('fills verification template', () => {
const result = runGsdTools('template fill verification --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-VERIFICATION.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('Observable Truths'), 'should have truths section');
assert.ok(content.includes('Required Artifacts'), 'should have artifacts section');
});
test('rejects existing file', () => {
// Create the file first
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing');
const result = runGsdTools('template fill summary --phase 1', tmpDir);
assert.ok(result.success); // outputs JSON, doesn't crash
const out = JSON.parse(result.output);
assert.ok(out.error, 'should report error for existing file');
assert.ok(out.error.includes('already exists'));
});
test('errors on unknown template type', () => {
const result = runGsdTools('template fill bogus --phase 1', tmpDir);
assert.ok(!result.success, 'should fail for unknown type');
assert.ok(result.error.includes('Unknown template type'));
});
test('errors when phase not found', () => {
const result = runGsdTools('template fill summary --phase 99', tmpDir);
assert.ok(result.success);
const out = JSON.parse(result.output);
assert.ok(out.error, 'should report phase not found');
});
test('respects --plan option for plan number', () => {
const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`);
});
});