From 92e5c04e00d4a71c6cbe8505a30302fb6b5bec11 Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:20:19 +0000 Subject: [PATCH 01/43] feat: add Cursor CLI runtime support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Cursor as a fifth supported runtime alongside Claude Code, OpenCode, Gemini, and Codex. Cursor uses skills (~/.cursor/skills/gsd-*/SKILL.md) like Codex, with tool name mappings (Bash→Shell, Edit→StrReplace), subagent type conversion, and full Claude-to-Cursor content adaptation including project conventions (CLAUDE.md→.cursor/rules/, .claude/skills/ →.cursor/skills/) and brand references. Includes install, uninstall, interactive prompt, help text, manifest tracking, and .cjs/.js utility script conversion. Made-with: Cursor --- bin/install.js | 279 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 263 insertions(+), 16 deletions(-) diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..ebbaa4c68 100755 --- a/bin/install.js +++ b/bin/install.js @@ -64,6 +64,7 @@ const hasGemini = args.includes('--gemini'); const hasCodex = args.includes('--codex'); const hasCopilot = args.includes('--copilot'); const hasAntigravity = args.includes('--antigravity'); +const hasCursor = args.includes('--cursor'); const hasBoth = args.includes('--both'); // Legacy flag, keeps working const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); @@ -71,7 +72,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { - selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']; + selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']; } else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; } else { @@ -81,6 +82,7 @@ if (hasAll) { if (hasCodex) selectedRuntimes.push('codex'); if (hasCopilot) selectedRuntimes.push('copilot'); if (hasAntigravity) selectedRuntimes.push('antigravity'); + if (hasCursor) selectedRuntimes.push('cursor'); } // WSL + Windows Node.js detection @@ -124,6 +126,7 @@ function getDirName(runtime) { if (runtime === 'gemini') return '.gemini'; if (runtime === 'codex') return '.codex'; if (runtime === 'antigravity') return '.agent'; + if (runtime === 'cursor') return '.cursor'; return '.claude'; } @@ -151,6 +154,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (!isGlobal) return "'.agent'"; return "'.gemini', 'antigravity'"; } + if (runtime === 'cursor') return "'.cursor'"; return "'.claude'"; } @@ -237,6 +241,18 @@ function getGlobalDir(runtime, explicitDir = null) { return path.join(os.homedir(), '.gemini', 'antigravity'); } + if (runtime === 'cursor') { + // Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor + if (explicitDir) { + return expandTilde(explicitDir); + } + if (process.env.CURSOR_CONFIG_DIR) { + return expandTilde(process.env.CURSOR_CONFIG_DIR); + } + return path.join(os.homedir(), '.cursor'); + } + + // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { return expandTilde(explicitDir); @@ -257,7 +273,7 @@ const banner = '\n' + '\n' + ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, and Antigravity by TÂCHES.\n'; + ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, and Cursor by TÂCHES.\n'; // Parse --config-dir argument function parseConfigDirArg() { @@ -295,7 +311,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`); process.exit(0); } @@ -717,6 +733,123 @@ 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) { + let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { + return `gsd-${String(commandName).toLowerCase()}`; + }); + converted = converted.replace(/\/gsd-help\b/g, 'gsd-help'); + return converted; +} + +function convertClaudeToCursorMarkdown(content) { + let converted = convertSlashCommandsToCursorSkillMentions(content); + // Replace tool name references in body text + converted = converted.replace(/\bBash\(/g, 'Shell('); + converted = converted.replace(/\bEdit\(/g, 'StrReplace('); + converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); + // Replace subagent_type from Claude to Cursor format + converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); + converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); + // Replace project-level Claude conventions with Cursor equivalents + converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`'); + converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/'); + converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`'); + converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/'); + converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/'); + // Remove Claude Code-specific bug workarounds before brand replacement + converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); + converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); + // Replace "Claude Code" brand references with "Cursor" + converted = converted.replace(/\bClaude Code\b/g, 'Cursor'); + return converted; +} + +function getCursorSkillAdapterHeader(skillName) { + return ` +## A. Skill Invocation +- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. +- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. +- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. + +## B. User Prompting +When the workflow needs user input, prompt the user conversationally: +- Present options as a numbered list in your response text +- Ask the user to reply with their choice +- For multi-select, ask for comma-separated numbers + +## C. Tool Usage +Use these Cursor tools when executing GSD workflows: +- \`Shell\` for running commands (terminal operations) +- \`StrReplace\` for editing existing files +- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed + +## D. Subagent Spawning +When the workflow needs to spawn a subagent: +- Use \`Task(subagent_type="generalPurpose", ...)\` +- The \`model\` parameter maps to Cursor's model options (e.g., "fast") +`; +} + +function convertClaudeCommandToCursorSkill(content, skillName) { + const converted = convertClaudeToCursorMarkdown(content); + const { frontmatter, body } = extractFrontmatterAndBody(converted); + let description = `Run GSD workflow ${skillName}.`; + if (frontmatter) { + const maybeDescription = extractFrontmatterField(frontmatter, 'description'); + if (maybeDescription) { + description = maybeDescription; + } + } + description = toSingleLine(description); + const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; + const adapter = getCursorSkillAdapterHeader(skillName); + + return `---\nname: ${yamlQuote(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: ${yamlQuote(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()}`; @@ -1445,6 +1578,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 +1747,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 +1804,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 +1820,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:([a-z0-9-]+)/gi, (_, cmd) => `gsd-${cmd.toLowerCase()}`); + 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 +1920,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 +1938,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 +1965,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 +1979,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 +2017,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 +2068,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 +2493,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 +2505,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 +2518,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 +2596,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 +2619,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 +2644,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 +2712,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 +2786,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 +2821,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 @@ -2705,6 +2941,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 +3029,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 +3040,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 +3055,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 +3146,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') { From 31a93e2da726964f0b09994b285034baaa58f1aa Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:07 -0400 Subject: [PATCH 02/43] docs: document inherit profile for non-Anthropic providers (#1036) (#1185) Users on OpenRouter or local models get unexpected API costs because GSD's default 'balanced' profile spawns specific Anthropic models for subagents. The 'inherit' profile exists but wasn't well-documented for this use case. Changes: - model-profiles.md: add 'Using Non-Anthropic Models' section explaining when and how to use inherit profile - model-profiles.md: update inherit description to mention OpenRouter and local models - settings.md: update Inherit option description to mention OpenRouter and local models (was only mentioning OpenCode) Closes #1036 --- get-shit-done/references/model-profiles.md | 18 ++++++++++++++++++ get-shit-done/workflows/settings.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/get-shit-done/references/model-profiles.md b/get-shit-done/references/model-profiles.md index e4d5f0649..97a92b75d 100644 --- a/get-shit-done/references/model-profiles.md +++ b/get-shit-done/references/model-profiles.md @@ -40,8 +40,26 @@ Model profiles control which Claude model each GSD agent uses. This allows balan **inherit** - Follow the current session model - All agents resolve to `inherit` - Best when you switch models interactively (for example OpenCode `/model`) +- **Required when using non-Anthropic providers** (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs - Use when: you want GSD to follow your currently selected runtime model +## Using Non-Anthropic Models (OpenRouter, Local, etc.) + +If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic provider, set the `inherit` profile to prevent GSD from calling Anthropic models for subagents: + +```bash +# Via settings command +/gsd:settings +# → Select "Inherit" for model profile + +# Or manually in .planning/config.json +{ + "model_profile": "inherit" +} +``` + +Without `inherit`, GSD's default `balanced` profile spawns specific Anthropic models (`opus`, `sonnet`, `haiku`) for each agent type, which can result in additional API costs through your non-Anthropic provider. + ## Resolution Logic Orchestrators resolve model before spawning: diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 7fc344559..b540cff61 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -49,7 +49,7 @@ AskUserQuestion([ { label: "Quality", description: "Opus everywhere except verification (highest cost)" }, { label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" }, { label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" }, - { label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" } + { label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" } ] }, { From 94b83759afb8b43681a21e1e2979fb465c4c2ade Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:21 -0400 Subject: [PATCH 03/43] fix: use arrays for RUNTIME_DIRS to fix zsh word-splitting (#1173) (#1183) The version detection script in update.md used a space-separated string for RUNTIME_DIRS and iterated with `for entry in $RUNTIME_DIRS`. This relies on word-splitting which works in bash but fails in zsh (zsh does not word-split unquoted variables by default), causing the entire string to be treated as one entry and detection to fall through to UNKNOWN. Fix: convert RUNTIME_DIRS and ORDERED_RUNTIME_DIRS from space-separated strings to proper arrays, and iterate with ${array[@]} syntax which works correctly in both bash and zsh. Closes #1173 --- get-shit-done/workflows/update.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/get-shit-done/workflows/update.md b/get-shit-done/workflows/update.md index 7d276eae4..fa910c927 100644 --- a/get-shit-done/workflows/update.md +++ b/get-shit-done/workflows/update.md @@ -20,8 +20,11 @@ First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it. ```bash -# Runtime candidates: ":" -RUNTIME_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex" +# Runtime candidates: ":" stored as an array. +# Using an array instead of a space-separated string ensures correct +# iteration in both bash and zsh (zsh does not word-split unquoted +# variables by default). Fixes #1173. +RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" ) # PREFERRED_RUNTIME should be set from execution_context before running this block. # If not set, infer from runtime env vars; fallback to claude. @@ -40,23 +43,23 @@ if [ -z "$PREFERRED_RUNTIME" ]; then fi # Reorder entries so preferred runtime is checked first. -ORDERED_RUNTIME_DIRS="" -for entry in $RUNTIME_DIRS; do +ORDERED_RUNTIME_DIRS=() +for entry in "${RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then - ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry" + ORDERED_RUNTIME_DIRS+=( "$entry" ) fi done -for entry in $RUNTIME_DIRS; do +for entry in "${RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then - ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry" + ORDERED_RUNTIME_DIRS+=( "$entry" ) fi done # Check local first (takes priority only if valid and distinct from global) LOCAL_VERSION_FILE="" LOCAL_MARKER_FILE="" LOCAL_DIR="" LOCAL_RUNTIME="" -for entry in $ORDERED_RUNTIME_DIRS; do +for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" dir="${entry#*:}" if [ -f "./$dir/get-shit-done/VERSION" ] || [ -f "./$dir/get-shit-done/workflows/update.md" ]; then @@ -69,7 +72,7 @@ for entry in $ORDERED_RUNTIME_DIRS; do done GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME="" -for entry in $ORDERED_RUNTIME_DIRS; do +for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" dir="${entry#*:}" if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then From 9acfa4bffc67de82d796feeda63fd6bde2352e68 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:40 -0400 Subject: [PATCH 04/43] fix: add sequential fallback for map-codebase on runtimes without Task tool (#1174) (#1184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtimes like Antigravity don't have a Task tool for spawning subagents. When the agent encounters Task() calls, it falls back to browser_subagent which is meant for web browsing, not code analysis — causing gsd-map-codebase to fail. This adds: 1. A detect_runtime_capabilities step before spawn_agents 2. An explicit warning to NEVER use browser_subagent for code analysis 3. A sequential_mapping fallback step that performs all 4 mapping passes inline using file system tools when Task is unavailable Closes #1174 --- get-shit-done/workflows/map-codebase.md | 54 ++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index 901b14511..6398e5d8b 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -82,12 +82,25 @@ mkdir -p .planning/codebase Continue to spawn_agents. - + +Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation. + +**Runtimes with Task tool:** Claude Code, Cursor (native subagent support) +**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others + +**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis): + +→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead. + +**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context. + + + Spawn 4 parallel gsd-codebase-mapper agents. Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution. -**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly. +**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly. **Agent 1: Tech Focus** @@ -195,6 +208,37 @@ If any agent failed, note the failure and continue with successful documents. Continue to verify_output. + +When the `Task` tool is unavailable, perform codebase mapping sequentially in the current context. This replaces `spawn_agents` and `collect_confirmations`. + +**IMPORTANT:** Do NOT use `browser_subagent`, `Explore`, or any browser-based tool. Use only file system tools (Read, Bash, Write, Grep, Glob, list_dir, view_file, grep_search, or equivalent tools available in your runtime). + +Perform all 4 mapping passes sequentially: + +**Pass 1: Tech Focus** +- Explore package.json/Cargo.toml/go.mod/requirements.txt, config files, dependency trees +- Write `.planning/codebase/STACK.md` — Languages, runtime, frameworks, dependencies, configuration +- Write `.planning/codebase/INTEGRATIONS.md` — External APIs, databases, auth providers, webhooks + +**Pass 2: Architecture Focus** +- Explore directory structure, entry points, module boundaries, data flow +- Write `.planning/codebase/ARCHITECTURE.md` — Pattern, layers, data flow, abstractions, entry points +- Write `.planning/codebase/STRUCTURE.md` — Directory layout, key locations, naming conventions + +**Pass 3: Quality Focus** +- Explore code style, error handling patterns, test files, CI config +- Write `.planning/codebase/CONVENTIONS.md` — Code style, naming, patterns, error handling +- Write `.planning/codebase/TESTING.md` — Framework, structure, mocking, coverage + +**Pass 4: Concerns Focus** +- Explore TODOs, known issues, fragile areas, security patterns +- Write `.planning/codebase/CONCERNS.md` — Tech debt, bugs, security, performance, fragile areas + +Use the same document templates as the `gsd-codebase-mapper` agent. Include actual file paths formatted with backticks. + +Continue to verify_output. + + Verify all documents created successfully: @@ -307,10 +351,10 @@ End workflow. - .planning/codebase/ directory created -- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true -- Agents write documents directly (orchestrator doesn't receive document contents) -- Read agent output files to collect confirmations +- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true +- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent) - All 7 codebase documents exist +- No empty documents (each should have >20 lines) - Clear completion summary with line counts - User offered clear next steps in GSD style From 93dc3d134f0bd6a39f20916d873f06ec6075acd6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:55 -0400 Subject: [PATCH 05/43] test: add coverage for model-profiles, template, profile-pipeline, profile-output (#1170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New test files for 4 previously untested modules: model-profiles.test.cjs (15 tests): - MODEL_PROFILES data integrity (all agents, all profiles, valid aliases) - VALID_PROFILES list validation - getAgentToModelMapForProfile (balanced, budget, quality, agent count) - formatAgentToModelMapAsTable (header, separator, column alignment) template.test.cjs (11 tests): - template select: minimal/standard/complex heuristics, fallback - template fill: summary/plan/verification generation, --plan option - Error paths: existing file, unknown type, missing phase profile-pipeline.test.cjs (7 tests): - scan-sessions: empty dir, synthetic project, multi-session - extract-messages: user message extraction, meta/internal filtering - profile-questionnaire: structure validation profile-output.test.cjs (13 tests): - PROFILING_QUESTIONS data (fields, options, uniqueness) - CLAUDE_INSTRUCTIONS coverage (dimensions, instruction mapping) - write-profile: analysis JSON → USER-PROFILE.md - generate-claude-md: --auto generation, --force protection - generate-dev-preferences: analysis → preferences command Test count: 744 → 798 (+54 new tests, 0 failures) --- tests/model-profiles.test.cjs | 134 ++++++++++++++++++++++ tests/profile-output.test.cjs | 197 ++++++++++++++++++++++++++++++++ tests/profile-pipeline.test.cjs | 160 ++++++++++++++++++++++++++ tests/template.test.cjs | 186 ++++++++++++++++++++++++++++++ 4 files changed, 677 insertions(+) create mode 100644 tests/model-profiles.test.cjs create mode 100644 tests/profile-output.test.cjs create mode 100644 tests/profile-pipeline.test.cjs create mode 100644 tests/template.test.cjs diff --git a/tests/model-profiles.test.cjs b/tests/model-profiles.test.cjs new file mode 100644 index 000000000..55fd1cf00 --- /dev/null +++ b/tests/model-profiles.test.cjs @@ -0,0 +1,134 @@ +/** + * Model Profiles Tests + * + * Tests for MODEL_PROFILES data structure, VALID_PROFILES list, + * formatAgentToModelMapAsTable, and getAgentToModelMapForProfile. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); + +const { + MODEL_PROFILES, + VALID_PROFILES, + formatAgentToModelMapAsTable, + getAgentToModelMapForProfile, +} = require('../get-shit-done/bin/lib/model-profiles.cjs'); + +// ─── MODEL_PROFILES data integrity ──────────────────────────────────────────── + +describe('MODEL_PROFILES', () => { + test('contains all expected GSD agents', () => { + const expectedAgents = [ + 'gsd-planner', 'gsd-roadmapper', 'gsd-executor', + 'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer', + 'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier', + 'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor', + 'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor', + ]; + for (const agent of expectedAgents) { + assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`); + } + }); + + test('every agent has quality, balanced, and budget profiles', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.ok(profiles.quality, `${agent} missing quality profile`); + assert.ok(profiles.balanced, `${agent} missing balanced profile`); + assert.ok(profiles.budget, `${agent} missing budget profile`); + } + }); + + test('all profile values are valid model aliases', () => { + const validModels = ['opus', 'sonnet', 'haiku']; + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + for (const [profile, model] of Object.entries(profiles)) { + assert.ok( + validModels.includes(model), + `${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}` + ); + } + } + }); + + test('quality profile never uses haiku', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.notStrictEqual( + profiles.quality, 'haiku', + `${agent} quality profile should not use haiku` + ); + } + }); +}); + +// ─── VALID_PROFILES ─────────────────────────────────────────────────────────── + +describe('VALID_PROFILES', () => { + test('contains quality, balanced, and budget', () => { + assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']); + }); + + test('is derived from MODEL_PROFILES keys', () => { + const fromData = Object.keys(MODEL_PROFILES['gsd-planner']); + assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort()); + }); +}); + +// ─── getAgentToModelMapForProfile ───────────────────────────────────────────── + +describe('getAgentToModelMapForProfile', () => { + test('returns correct models for balanced profile', () => { + const map = getAgentToModelMapForProfile('balanced'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-codebase-mapper'], 'haiku'); + assert.strictEqual(map['gsd-verifier'], 'sonnet'); + }); + + test('returns correct models for budget profile', () => { + const map = getAgentToModelMapForProfile('budget'); + assert.strictEqual(map['gsd-planner'], 'sonnet'); + assert.strictEqual(map['gsd-phase-researcher'], 'haiku'); + }); + + test('returns correct models for quality profile', () => { + const map = getAgentToModelMapForProfile('quality'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-executor'], 'opus'); + }); + + test('returns all agents in the map', () => { + const map = getAgentToModelMapForProfile('balanced'); + const agentCount = Object.keys(MODEL_PROFILES).length; + assert.strictEqual(Object.keys(map).length, agentCount); + }); +}); + +// ─── formatAgentToModelMapAsTable ───────────────────────────────────────────── + +describe('formatAgentToModelMapAsTable', () => { + test('produces a table with header and separator', () => { + const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' }; + const table = formatAgentToModelMapAsTable(map); + assert.ok(table.includes('Agent'), 'should have Agent header'); + assert.ok(table.includes('Model'), 'should have Model header'); + assert.ok(table.includes('─'), 'should have separator line'); + assert.ok(table.includes('gsd-planner'), 'should list agent'); + assert.ok(table.includes('opus'), 'should list model'); + }); + + test('pads columns correctly', () => { + const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' }; + const table = formatAgentToModelMapAsTable(map); + const lines = table.split('\n').filter(l => l.trim()); + // Separator line uses ┼, data/header lines use │ + const dataLines = lines.filter(l => l.includes('│')); + const pipePositions = dataLines.map(l => l.indexOf('│')); + const unique = [...new Set(pipePositions)]; + assert.strictEqual(unique.length, 1, 'all data lines should align on │'); + }); + + test('handles empty map', () => { + const table = formatAgentToModelMapAsTable({}); + assert.ok(table.includes('Agent'), 'should still have header'); + }); +}); diff --git a/tests/profile-output.test.cjs b/tests/profile-output.test.cjs new file mode 100644 index 000000000..3001195f5 --- /dev/null +++ b/tests/profile-output.test.cjs @@ -0,0 +1,197 @@ +/** + * Profile Output Tests + * + * Tests for profile rendering commands and PROFILING_QUESTIONS data. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs'); + +const { + PROFILING_QUESTIONS, + CLAUDE_INSTRUCTIONS, +} = require('../get-shit-done/bin/lib/profile-output.cjs'); + +// ─── PROFILING_QUESTIONS data ───────────────────────────────────────────────── + +describe('PROFILING_QUESTIONS', () => { + test('is a non-empty array', () => { + assert.ok(Array.isArray(PROFILING_QUESTIONS)); + assert.ok(PROFILING_QUESTIONS.length > 0); + }); + + test('each question has required fields', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok(q.dimension, `question missing dimension`); + assert.ok(q.header, `${q.dimension} missing header`); + assert.ok(q.question, `${q.dimension} missing question`); + assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`); + assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`); + } + }); + + test('each option has label, value, and rating', () => { + for (const q of PROFILING_QUESTIONS) { + for (const opt of q.options) { + assert.ok(opt.label, `${q.dimension} option missing label`); + assert.ok(opt.value, `${q.dimension} option missing value`); + assert.ok(opt.rating, `${q.dimension} option missing rating`); + } + } + }); + + test('all dimension keys are unique', () => { + const dims = PROFILING_QUESTIONS.map(q => q.dimension); + const unique = [...new Set(dims)]; + assert.strictEqual(dims.length, unique.length); + }); +}); + +// ─── CLAUDE_INSTRUCTIONS ────────────────────────────────────────────────────── + +describe('CLAUDE_INSTRUCTIONS', () => { + test('is a non-empty object', () => { + assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object'); + assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0); + }); + + test('each dimension has at least one instruction', () => { + for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) { + assert.ok(typeof instructions === 'object', `${dim} should be an object`); + assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`); + } + }); + + test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok( + CLAUDE_INSTRUCTIONS[q.dimension], + `${q.dimension} has questions but no CLAUDE_INSTRUCTIONS` + ); + } + }); +}); + +// ─── write-profile command ──────────────────────────────────────────────────── + +describe('write-profile command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('writes USER-PROFILE.md from analysis JSON', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + explanation_depth: { rating: 'concise', confidence: 'HIGH' }, + debugging_approach: { rating: 'fix-first', confidence: 'LOW' }, + ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' }, + vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' }, + frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' }, + learning_style: { rating: 'hands-on', confidence: 'MEDIUM' }, + }, + }; + + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.profile_path, 'should return profile_path'); + assert.ok(out.dimensions_scored > 0, 'should have scored dimensions'); + }); + + test('errors when --input is missing', () => { + const result = runGsdTools('write-profile --raw', tmpDir); + assert.ok(!result.success, 'should fail without --input'); + assert.ok(result.error.includes('--input'), 'should mention --input'); + }); +}); + +// ─── generate-claude-md command ─────────────────────────────────────────────── + +describe('generate-claude-md command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempGitProject(); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('generates CLAUDE.md with --auto flag', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + + if (fs.existsSync(outputPath)) { + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should have content'); + } + }); + + test('does not overwrite existing CLAUDE.md without --force', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n'); + + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + // Should merge, not overwrite + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should still have content'); + }); +}); + +// ─── generate-dev-preferences ───────────────────────────────────────────────── + +describe('generate-dev-preferences command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('errors when --analysis is missing', () => { + const result = runGsdTools('generate-dev-preferences --raw', tmpDir); + assert.ok(!result.success, 'should fail without --analysis'); + assert.ok(result.error.includes('--analysis'), 'should mention --analysis'); + }); + + test('generates preferences from analysis file', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + }, + }; + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.command_path || out.command_name, 'should return command output'); + }); +}); diff --git a/tests/profile-pipeline.test.cjs b/tests/profile-pipeline.test.cjs new file mode 100644 index 000000000..e6e783412 --- /dev/null +++ b/tests/profile-pipeline.test.cjs @@ -0,0 +1,160 @@ +/** + * Profile Pipeline Tests + * + * Tests for session scanning, message extraction, and profile sampling. + * Uses synthetic session data in temp directories via --path override. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── scan-sessions ──────────────────────────────────────────────────────────── + +describe('scan-sessions command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns empty array for empty sessions directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + fs.mkdirSync(sessionsDir, { recursive: true }); + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return an array'); + assert.strictEqual(out.length, 0, 'should be empty'); + }); + + test('scans synthetic project directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'test-project-abc123'); + fs.mkdirSync(projectDir, { recursive: true }); + + // Create a synthetic session file + const sessionData = [ + JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }), + JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }), + ].join('\n'); + fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData); + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return array'); + assert.strictEqual(out.length, 1, 'should find 1 project'); + assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session'); + }); + + test('reports multiple sessions and sizes', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'multi-session-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + for (let i = 1; i <= 3; i++) { + const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() }); + fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n'); + } + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out[0].sessionCount, 3); + assert.ok(out[0].totalSize > 0, 'should have non-zero size'); + }); +}); + +// ─── extract-messages ───────────────────────────────────────────────────────── + +describe('extract-messages command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('extracts user messages from synthetic session', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'my-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() }, + { type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', isMeta: true, message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages'); + assert.strictEqual(out.project, 'my-project'); + assert.ok(out.output_file, 'should have output file path'); + }); + + test('filters out meta and internal messages', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'filter-test'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages'); + }); +}); + +// ─── profile-questionnaire ──────────────────────────────────────────────────── + +describe('profile-questionnaire command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns questionnaire structure', () => { + const result = runGsdTools('profile-questionnaire --raw', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.questions, 'should have questions array'); + assert.ok(out.questions.length > 0, 'should have at least one question'); + assert.ok(out.questions[0].dimension, 'each question should have a dimension'); + assert.ok(out.questions[0].options, 'each question should have options'); + }); +}); diff --git a/tests/template.test.cjs b/tests/template.test.cjs new file mode 100644 index 000000000..8d2ae3d51 --- /dev/null +++ b/tests/template.test.cjs @@ -0,0 +1,186 @@ +/** + * Template Tests + * + * Tests for cmdTemplateSelect (heuristic template selection) and + * cmdTemplateFill (summary, plan, verification template generation). + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── template select ────────────────────────────────────────────────────────── + +describe('template select command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create a phase directory with a plan + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('selects minimal template for simple plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Do the thing.', + '', + 'File: `src/index.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'minimal'); + assert.ok(out.template.includes('summary-minimal')); + }); + + test('selects standard template for moderate plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Create `src/auth/login.ts`', + '', + '### Task 2', + 'Create `src/auth/register.ts`', + '', + '### Task 3', + 'Update `src/routes/index.ts`', + '', + 'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + }); + + test('selects complex template for plan with decisions and many files', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + const lines = ['# Plan', '']; + for (let i = 1; i <= 6; i++) { + lines.push(`### Task ${i}`, `Do task ${i}.`, ''); + } + lines.push('Made a decision about architecture.', 'Another decision here.'); + for (let i = 1; i <= 8; i++) { + lines.push(`File: \`src/module${i}/index.ts\``); + } + fs.writeFileSync(planPath, lines.join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'complex'); + }); + + test('returns standard as fallback for nonexistent file', () => { + const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + assert.ok(out.error, 'should include error message'); + }); +}); + +// ─── template fill ──────────────────────────────────────────────────────────── + +describe('template fill command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('fills summary template', () => { + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-SUMMARY.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Phase 1'), 'should reference phase'); + assert.ok(content.includes('Accomplishments'), 'should have accomplishments section'); + }); + + test('fills plan template', () => { + const result = runGsdTools('template fill plan --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-PLAN.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Objective'), 'should have objective section'); + assert.ok(content.includes(''), 'should have task XML'); + }); + + test('fills verification template', () => { + const result = runGsdTools('template fill verification --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-VERIFICATION.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('Observable Truths'), 'should have truths section'); + assert.ok(content.includes('Required Artifacts'), 'should have artifacts section'); + }); + + test('rejects existing file', () => { + // Create the file first + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing'); + + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success); // outputs JSON, doesn't crash + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report error for existing file'); + assert.ok(out.error.includes('already exists')); + }); + + test('errors on unknown template type', () => { + const result = runGsdTools('template fill bogus --phase 1', tmpDir); + assert.ok(!result.success, 'should fail for unknown type'); + assert.ok(result.error.includes('Unknown template type')); + }); + + test('errors when phase not found', () => { + const result = runGsdTools('template fill summary --phase 99', tmpDir); + assert.ok(result.success); + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report phase not found'); + }); + + test('respects --plan option for plan number', () => { + const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`); + }); +}); From 1efc74af51df32cd3dcd1c15de6b9569562a1987 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:25 -0400 Subject: [PATCH 06/43] refactor(tests): consolidate runtime converters, remove duplicate tests, standardize helpers (#1169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test consolidation: - Merged gemini-config.test.cjs + opencode-agent-conversion.test.cjs into runtime-converters.test.cjs (single file for small runtime converters) - Removed 11 duplicate tests from core.test.cjs: - 5 comparePhaseNum tests (authoritative copies in phase.test.cjs) - 6 normalizePhaseName tests (authoritative copies in phase.test.cjs) - Added note in core.test.cjs pointing to phase.test.cjs as canonical location Standardization: - core.test.cjs now uses createTempProject()/cleanup() from helpers.cjs instead of inline fs.mkdtempSync/fs.rmSync patterns File count: 21 → 19 test files Test count: 755 → 744 (11 duplicates removed, 0 coverage lost) --- tests/core.test.cjs | 101 ++++-------------- tests/gemini-config.test.cjs | 47 -------- ...n.test.cjs => runtime-converters.test.cjs} | 54 ++++++++-- 3 files changed, 69 insertions(+), 133 deletions(-) delete mode 100644 tests/gemini-config.test.cjs rename tests/{opencode-agent-conversion.test.cjs => runtime-converters.test.cjs} (66%) diff --git a/tests/core.test.cjs b/tests/core.test.cjs index b20328f32..2f9d796f0 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -10,6 +10,7 @@ const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const { createTempProject, cleanup } = require('./helpers.cjs'); const { loadConfig, @@ -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', () => { diff --git a/tests/gemini-config.test.cjs b/tests/gemini-config.test.cjs deleted file mode 100644 index 794208427..000000000 --- a/tests/gemini-config.test.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** - * GSD Tools Tests - Gemini agent conversion - * - * Verifies Gemini-specific agent frontmatter conversion removes - * unsupported fields while preserving converted tools and body text. - */ - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe } = require('node:test'); -const assert = require('node:assert'); - -const { convertClaudeToGeminiAgent } = require('../bin/install.js'); - -describe('convertClaudeToGeminiAgent', () => { - test('drops unsupported skills frontmatter while keeping converted tools', () => { - const input = `--- -name: gsd-codebase-mapper -description: Explores codebase and writes structured analysis documents. -tools: Read, Bash, Grep, Glob, Write -color: cyan -skills: - - gsd-mapper-workflow ---- - - -Use \${PHASE} in shell examples. -`; - - const result = convertClaudeToGeminiAgent(input); - const frontmatter = result.split('---')[1] || ''; - - assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); - assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); - assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); - assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); - assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); - assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); - assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); - assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); - assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); - assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); - assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); - assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); - assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); - }); -}); diff --git a/tests/opencode-agent-conversion.test.cjs b/tests/runtime-converters.test.cjs similarity index 66% rename from tests/opencode-agent-conversion.test.cjs rename to tests/runtime-converters.test.cjs index 6ba62b54b..6491d9376 100644 --- a/tests/opencode-agent-conversion.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -1,19 +1,21 @@ /** - * 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) + * 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 = `--- @@ -141,3 +143,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 +--- + + +Use \${PHASE} in shell examples. +`; + + const result = convertClaudeToGeminiAgent(input); + const frontmatter = result.split('---')[1] || ''; + + assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); + assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); + assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); + assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); + assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); + assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); + assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); + assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); + assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); + assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); + assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); + assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); + assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); + }); +}); From 41f8cd48ed2e8a0a9fb319dc7612af1c059f3a14 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:35 -0400 Subject: [PATCH 07/43] feat: add /gsd:next command for automatic workflow advancement (#927) (#1167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a zero-friction command that detects the current project state and automatically invokes the next logical workflow step: - No phases → discuss first phase - Phase has no context → discuss - Phase has context but no plans → plan - Phase has plans but incomplete → execute - All plans complete → verify and complete phase - All phases complete → complete milestone - Paused → resume work No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine progression. Designed for multi-project workflows. Closes #927 --- commands/gsd/next.md | 24 ++++++++ get-shit-done/workflows/next.md | 97 +++++++++++++++++++++++++++++++++ tests/copilot-install.test.cjs | 4 +- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/next.md create mode 100644 get-shit-done/workflows/next.md diff --git a/commands/gsd/next.md b/commands/gsd/next.md new file mode 100644 index 000000000..e7d81c747 --- /dev/null +++ b/commands/gsd/next.md @@ -0,0 +1,24 @@ +--- +name: gsd:next +description: Automatically advance to the next logical step in the GSD workflow +allowed-tools: + - Read + - Bash + - Grep + - Glob + - SlashCommand +--- + +Detect the current project state and automatically invoke the next logical GSD workflow step. +No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next. + +Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead. + + + +@~/.claude/get-shit-done/workflows/next.md + + + +Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end. + diff --git a/get-shit-done/workflows/next.md b/get-shit-done/workflows/next.md new file mode 100644 index 000000000..80e2f3622 --- /dev/null +++ b/get-shit-done/workflows/next.md @@ -0,0 +1,97 @@ + +Detect current project state and automatically advance to the next logical GSD workflow step. +Reads project state to determine: discuss → plan → execute → verify → complete progression. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +Read project state to determine current position: + +```bash +# Get state snapshot +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state json 2>/dev/null || echo "{}" +``` + +Also read: +- `.planning/STATE.md` — current phase, progress, plan counts +- `.planning/ROADMAP.md` — milestone structure and phase list + +Extract: +- `current_phase` — which phase is active +- `plan_of` / `plans_total` — plan execution progress +- `progress` — overall percentage +- `status` — active, paused, etc. + +If no `.planning/` directory exists: +``` +No GSD project detected. Run `/gsd:new-project` to get started. +``` +Exit. + + + +Apply routing rules based on state: + +**Route 1: No phases exist yet → discuss** +If ROADMAP has phases but no phase directories exist on disk: +→ Next action: `/gsd:discuss-phase ` + +**Route 2: Phase exists but has no CONTEXT.md or RESEARCH.md → discuss** +If the current phase directory exists but has neither CONTEXT.md nor RESEARCH.md: +→ Next action: `/gsd:discuss-phase ` + +**Route 3: Phase has context but no plans → plan** +If the current phase has CONTEXT.md (or RESEARCH.md) but no PLAN.md files: +→ Next action: `/gsd:plan-phase ` + +**Route 4: Phase has plans but incomplete summaries → execute** +If plans exist but not all have matching summaries: +→ Next action: `/gsd:execute-phase ` + +**Route 5: All plans have summaries → verify and complete** +If all plans in the current phase have summaries: +→ Next action: `/gsd:verify-work` then `/gsd:complete-phase` + +**Route 6: Phase complete, next phase exists → advance** +If the current phase is complete and the next phase exists in ROADMAP: +→ Next action: `/gsd:discuss-phase ` + +**Route 7: All phases complete → complete milestone** +If all phases are complete: +→ Next action: `/gsd:complete-milestone` + +**Route 8: Paused → resume** +If STATE.md shows paused_at: +→ Next action: `/gsd:resume-work` + + + +Display the determination: + +``` +## GSD Next + +**Current:** Phase [N] — [name] | [progress]% +**Status:** [status description] + +▶ **Next step:** `/gsd:[command] [args]` + [One-line explanation of why this is the next step] +``` + +Then immediately invoke the determined command via SlashCommand. +Do not ask for confirmation — the whole point of `/gsd:next` is zero-friction advancement. + + + + + +- [ ] Project state correctly detected +- [ ] Next action correctly determined from routing rules +- [ ] Command invoked immediately without user confirmation +- [ ] Clear status shown before invoking + diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 158ea974e..88404019e 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 39, `expected 39 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 40, `expected 40 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 = 40; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 8520424a62d9af3e311b91856d68b188fe7f773e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:44 -0400 Subject: [PATCH 08/43] fix: replace curl with fetch() in verification examples for Windows compatibility (#899) (#1166) MSYS curl on Windows has SSL/TLS failures and path mangling issues. Replaced curl references in checkpoint and phase-prompt templates with Node.js fetch() which works cross-platform. Changes: - checkpoints.md: server readiness check uses fetch() instead of curl - checkpoints.md: added cross-platform note about curl vs fetch - checkpoints.md: verify tags use fetch instead of curl - phase-prompt.md: verify tags use fetch instead of curl Partially addresses #899 (patch 1 of 6) --- get-shit-done/references/checkpoints.md | 22 ++++++++++++---------- get-shit-done/templates/phase-prompt.md | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/get-shit-done/references/checkpoints.md b/get-shit-done/references/checkpoints.md index 232817480..23f90e99c 100644 --- a/get-shit-done/references/checkpoints.md +++ b/get-shit-done/references/checkpoints.md @@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Start dev server for verification Run `npm run dev` in background, wait for "ready" message, capture port - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Deploy to Vercel .vercel/, vercel.json Run `vercel --yes` to deploy - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 @@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Retry Vercel deployment Run `vercel --yes` (now authenticated) - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 ``` @@ -455,8 +455,8 @@ I'll verify: vercel whoami returns your account npm run dev & DEV_SERVER_PID=$! -# Wait for ready (max 30s) -timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done' +# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility +timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done' ``` **Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`). @@ -489,7 +489,9 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d | Auth error | Create auth gate checkpoint | | Network timeout | Retry with backoff, then checkpoint if persistent | -**Never present a checkpoint with broken verification environment.** If `curl localhost:3000` fails, don't ask user to "visit localhost:3000". +**Never present a checkpoint with broken verification environment.** If the local server isn't responding, don't ask user to "visit localhost:3000". + +> **Cross-platform note:** Use `node -e "fetch('http://localhost:3000').then(r=>console.log(r.status))"` instead of `curl` for health checks. `curl` is broken on Windows MSYS/Git Bash due to SSL/path mangling issues. ```xml @@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Fix server startup issue Investigate error, fix root cause, restart server - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server for auth testing Run `npm run dev` in background, wait for ready signal - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server Run `npm run dev` in background - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Deploy to Vercel Run `vercel --yes`. Capture URL. - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 diff --git a/get-shit-done/templates/phase-prompt.md b/get-shit-done/templates/phase-prompt.md index 6d23160dd..b242dc15e 100644 --- a/get-shit-done/templates/phase-prompt.md +++ b/get-shit-done/templates/phase-prompt.md @@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components. Task 2: Create User API endpoints src/features/user/api.ts GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model. - curl tests pass for all endpoints + fetch tests pass for all endpoints All CRUD operations work @@ -407,7 +407,7 @@ Output: Working dashboard component. Start dev server Run `npm run dev` in background, wait for ready - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 From 14c1dd845b993e612f7eb31d31ff720bcc18f744 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:51 -0400 Subject: [PATCH 09/43] fix(build): add syntax validation to hook build script (#1165) Prevents shipping hooks with JavaScript SyntaxError (like the duplicate const cwd declaration that caused PostToolUse errors for all users in v1.25.1). The build script now validates each hook file's syntax via vm.Script before copying to dist/. If any hook has a SyntaxError, the build fails with a clear error message and exits non-zero, blocking npm publish. Refs #1107, #1109, #1125, #1161 --- scripts/build-hooks.js | 43 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index ffb60b0ff..dda263e4e 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -1,10 +1,14 @@ #!/usr/bin/env node /** * Copy GSD hooks to dist for installation. + * Validates JavaScript syntax before copying to prevent shipping broken hooks. + * See #1107, #1109, #1125, #1161 — a duplicate const declaration shipped + * in dist and caused PostToolUse hook errors for all users. */ const fs = require('fs'); const path = require('path'); +const vm = require('vm'); const HOOKS_DIR = path.join(__dirname, '..', 'hooks'); const DIST_DIR = path.join(HOOKS_DIR, 'dist'); @@ -16,13 +20,34 @@ const HOOKS_TO_COPY = [ 'gsd-statusline.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 +57,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.'); From e7198f419f89faa6d72f9ca4e35eb417d132d0ae Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:20 -0400 Subject: [PATCH 10/43] fix: hook version tracking, stale hook detection, stdin timeout, and session-report command (#1153, #1157, #1161, #1162) (#1163) * fix: hook version tracking, stale hook detection, and stdin timeout increase - Add gsd-hook-version header to all hook files for version tracking (#1153) - Install.js now stamps current version into hooks during installation - gsd-check-update.js detects stale hooks by comparing version headers - gsd-statusline.js shows warning when stale hooks are detected - Increase context monitor stdin timeout from 3s to 10s (#1162) - Set +x permission on hook files during installation (#1162) Fixes #1153, #1162, #1161 * feat: add /gsd:session-report command for post-session summary generation Adds a new command that generates SESSION_REPORT.md with: - Work performed summary (phases touched, commits, files changed) - Key outcomes and decisions made - Active blockers and open items - Estimated resource usage metrics Reports are written to .planning/reports/ with date-stamped filenames. Closes #1157 * test: update expected skill count from 39 to 40 for new session-report command --- bin/install.js | 4 + commands/gsd/session-report.md | 19 +++ get-shit-done/workflows/session-report.md | 146 ++++++++++++++++++++++ hooks/gsd-check-update.js | 34 ++++- hooks/gsd-context-monitor.js | 10 +- hooks/gsd-statusline.js | 4 + 6 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 commands/gsd/session-report.md create mode 100644 get-shit-done/workflows/session-report.md diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..95b50d7e9 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2606,10 +2606,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); } diff --git a/commands/gsd/session-report.md b/commands/gsd/session-report.md new file mode 100644 index 000000000..a0eb1d6ef --- /dev/null +++ b/commands/gsd/session-report.md @@ -0,0 +1,19 @@ +--- +name: gsd:session-report +description: Generate a session report with token usage estimates, work summary, and outcomes +allowed-tools: + - Read + - Bash + - Write +--- + +Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review. + + + +@~/.claude/get-shit-done/workflows/session-report.md + + + +Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end. + diff --git a/get-shit-done/workflows/session-report.md b/get-shit-done/workflows/session-report.md new file mode 100644 index 000000000..f336edc08 --- /dev/null +++ b/get-shit-done/workflows/session-report.md @@ -0,0 +1,146 @@ + +Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +Collect session data from available sources: + +1. **STATE.md** — current phase, milestone, progress, blockers, decisions +2. **Git log** — commits made during this session (last 24h or since last report) +3. **Plan/Summary files** — plans executed, summaries written +4. **ROADMAP.md** — milestone context and phase goals + +```bash +# Get recent commits (last 24 hours) +git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits" + +# Count files changed +git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available" +``` + +Read `.planning/STATE.md` to get: +- Current milestone and phase +- Progress percentage +- Active blockers +- Recent decisions + +Read `.planning/ROADMAP.md` to get milestone name and goals. + +Check for existing reports: +```bash +ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports" +``` + + + +Estimate token usage from observable signals: + +- Count of tool calls is not directly available, so estimate from git activity and file operations +- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks + +Estimation heuristics: +- Each commit ≈ 1 plan cycle (research + plan + execute + verify) +- Each plan file ≈ 2,000-5,000 tokens of agent context +- Each summary file ≈ 1,000-2,000 tokens generated +- Subagent spawns multiply by ~1.5x per agent type used + + + +Create the report directory and file: + +```bash +mkdir -p .planning/reports +``` + +Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist): + +```markdown +# GSD Session Report + +**Generated:** [timestamp] +**Project:** [from PROJECT.md title or directory name] +**Milestone:** [N] — [milestone name from ROADMAP.md] + +--- + +## Session Summary + +**Duration:** [estimated from first to last commit timestamp, or "Single session"] +**Phase Progress:** [from STATE.md] +**Plans Executed:** [count of summaries written this session] +**Commits Made:** [count from git log] + +## Work Performed + +### Phases Touched +[List phases worked on with brief description of what was done] + +### Key Outcomes +[Bullet list of concrete deliverables: files created, features implemented, bugs fixed] + +### Decisions Made +[From STATE.md decisions table, if any were added this session] + +## Files Changed + +[Summary of files modified, created, deleted — from git diff stat] + +## Blockers & Open Items + +[Active blockers from STATE.md] +[Any TODO items created during session] + +## Estimated Resource Usage + +| Metric | Estimate | +|--------|----------| +| Commits | [N] | +| Files changed | [N] | +| Plans executed | [N] | +| Subagents spawned | [estimated] | + +> **Note:** Token and cost estimates require API-level instrumentation. +> These metrics reflect observable session activity only. + +--- + +*Generated by `/gsd:session-report`* +``` + + + +Show the user: + +``` +## Session Report Generated + +📄 `.planning/reports/[filename].md` + +### Highlights +- **Commits:** [N] +- **Files changed:** [N] +- **Phase progress:** [X]% +- **Plans executed:** [N] +``` + +If this is the first report, mention: +``` +💡 Run `/gsd:session-report` at the end of each session to build a history of project activity. +``` + + + + + +- [ ] Session data gathered from STATE.md, git log, and plan files +- [ ] Report written to .planning/reports/ +- [ ] Report includes work summary, outcomes, and file changes +- [ ] Filename includes date to prevent overwrites +- [ ] Result summary displayed to user + diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js index b9a6075ed..510302fb3 100755 --- a/hooks/gsd-check-update.js +++ b/hooks/gsd-check-update.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Check for GSD updates in background, write result to cache // Called by SessionStart hook - runs once per session @@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) { // Run check in background (spawn background process, windowsHide prevents console flash) const child = spawn(process.execPath, ['-e', ` const fs = require('fs'); + const path = require('path'); const { execSync } = require('child_process'); const cacheFile = ${JSON.stringify(cacheFile)}; @@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', ` // Check project directory first (local install), then global let installed = '0.0.0'; + let configDir = ''; try { if (fs.existsSync(projectVersionFile)) { installed = fs.readFileSync(projectVersionFile, 'utf8').trim(); + configDir = path.dirname(path.dirname(projectVersionFile)); } else if (fs.existsSync(globalVersionFile)) { installed = fs.readFileSync(globalVersionFile, 'utf8').trim(); + configDir = path.dirname(path.dirname(globalVersionFile)); } } catch (e) {} + // Check for stale hooks — compare hook version headers against installed VERSION + let staleHooks = []; + if (configDir) { + const hooksDir = path.join(configDir, 'hooks'); + try { + if (fs.existsSync(hooksDir)) { + const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js')); + for (const hookFile of hookFiles) { + try { + const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8'); + const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/); + if (versionMatch) { + const hookVersion = versionMatch[1].trim(); + if (hookVersion !== installed && !hookVersion.includes('{{')) { + staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed }); + } + } else { + // No version header at all — definitely stale (pre-version-tracking) + staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed }); + } + } catch (e) {} + } + } + } catch (e) {} + } + let latest = null; try { latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim(); @@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', ` update_available: latest && installed !== latest, installed, latest: latest || 'unknown', - checked: Math.floor(Date.now() / 1000) + checked: Math.floor(Date.now() / 1000), + stale_hooks: staleHooks.length > 0 ? staleHooks : undefined }; fs.writeFileSync(cacheFile, JSON.stringify(result)); diff --git a/hooks/gsd-context-monitor.js b/hooks/gsd-context-monitor.js index d7a5eff06..ae1bbf9a3 100644 --- a/hooks/gsd-context-monitor.js +++ b/hooks/gsd-context-monitor.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool) // Reads context metrics from the statusline bridge file and injects // warnings when context usage is high. This makes the AGENT aware of @@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s const DEBOUNCE_CALLS = 5; // min tool uses between warnings let input = ''; -// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on -// Windows/Git Bash), exit silently instead of hanging until Claude Code -// kills the process and reports "hook error". See #775. -const stdinTimeout = setTimeout(() => process.exit(0), 3000); +// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on +// Windows/Git Bash, or slow Claude Code piping during large outputs), +// exit silently instead of hanging until Claude Code kills the process +// and reports "hook error". See #775, #1162. +const stdinTimeout = setTimeout(() => process.exit(0), 10000); process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => { diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index d88ca4a2c..ae7025b99 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Claude Code Statusline - GSD Edition // Shows: model | current task | directory | context usage @@ -99,6 +100,9 @@ process.stdin.on('end', () => { if (cache.update_available) { gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ '; } + if (cache.stale_hooks && cache.stale_hooks.length > 0) { + gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ '; + } } catch (e) {} } From 26d742c5485bcc38e0894b4552399751c50def58 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:31 -0400 Subject: [PATCH 11/43] fix(core): replace negative-heuristic stripShippedMilestones with positive milestone lookup (#1145) (#1146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stripShippedMilestones() uses a negative heuristic: strip all
blocks, assume what remains is the current milestone. This breaks when agents accidentally wrap the current milestone in
for collapsibility — all downstream consumers then see an empty milestone. Observed failure: cmdPhaseComplete() returns is_last_phase: true and next_phase: null for non-final phases because the current milestone's phases were stripped along with shipped ones. Added extractCurrentMilestone(content, cwd) — a positive lookup that: 1. Reads the current milestone version from STATE.md frontmatter 2. Falls back to 🚧 in-progress marker in ROADMAP.md 3. Finds the section heading matching that version 4. Returns only that section's content 5. Falls back to stripShippedMilestones() if version can't be determined Updated 12 call sites across 6 files to use extractCurrentMilestone: - core.cjs: getRoadmapPhaseInternal(), getMilestonePhaseFilter() - phase.cjs: cmdPhaseAdd(), cmdPhaseInsert(), cmdPhaseComplete() (2 sites) - roadmap.cjs: cmdRoadmapGetPhase(), cmdRoadmapAnalyze() - commands.cjs: stats/progress display - verify.cjs: phase verification (2 sites) - init.cjs: project initialization Kept stripShippedMilestones() for: - getMilestoneInfo() — determines the version itself, can't use positive lookup - replaceInCurrentMilestone() — write operations, conservative boundary - extractCurrentMilestone() fallback — when no version available All 755 tests pass. Fixes #1145 --- get-shit-done/bin/lib/commands.cjs | 4 +- get-shit-done/bin/lib/core.cjs | 90 +++++++++++++++++++++++++++++- get-shit-done/bin/lib/init.cjs | 6 +- get-shit-done/bin/lib/phase.cjs | 10 ++-- get-shit-done/bin/lib/roadmap.cjs | 6 +- get-shit-done/bin/lib/verify.cjs | 6 +- 6 files changed, 104 insertions(+), 18 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 73decc8bd..d7109df19 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -547,7 +547,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) { diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..16d6e2726 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -420,6 +420,91 @@ function stripShippedMilestones(content) { return content.replace(/
[\s\S]*?<\/details>/gi, ''); } +/** + * Extract the current milestone section from ROADMAP.md by positive lookup. + * + * Instead of stripping
blocks (negative heuristic that breaks if + * agents wrap the current milestone in
), this finds the section + * matching the current milestone version and returns only that content. + * + * Falls back to stripShippedMilestones() if: + * - cwd is not provided + * - STATE.md doesn't exist or has no milestone field + * - Version can't be found in ROADMAP.md + * + * @param {string} content - Full ROADMAP.md content + * @param {string} [cwd] - Working directory for reading STATE.md + * @returns {string} Content scoped to current milestone + */ +function extractCurrentMilestone(content, cwd) { + if (!cwd) return stripShippedMilestones(content); + + // 1. Get current milestone version from STATE.md frontmatter + let version = null; + try { + const statePath = path.join(cwd, '.planning', 'STATE.md'); + if (fs.existsSync(statePath)) { + const stateRaw = fs.readFileSync(statePath, 'utf-8'); + const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m); + if (milestoneMatch) { + version = milestoneMatch[1].trim(); + } + } + } catch {} + + // 2. Fallback: derive version from getMilestoneInfo pattern in ROADMAP.md itself + if (!version) { + // Check for 🚧 in-progress marker + const inProgressMatch = content.match(/🚧\s*\*\*v(\d+\.\d+)\s/); + if (inProgressMatch) { + version = 'v' + inProgressMatch[1]; + } + } + + if (!version) return stripShippedMilestones(content); + + // 3. Find the section matching this version + // Match headings like: ## Roadmap v3.0: Name, ## v3.0 Name, etc. + const escapedVersion = escapeRegex(version); + const sectionPattern = new RegExp( + `(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`, + 'mi' + ); + const sectionMatch = content.match(sectionPattern); + + if (!sectionMatch) return stripShippedMilestones(content); + + const sectionStart = sectionMatch.index; + + // Find the end: next milestone heading at same or higher level, or EOF + // Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc. + const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length; + const restContent = content.slice(sectionStart + sectionMatch[0].length); + const nextMilestonePattern = new RegExp( + `^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, + 'mi' + ); + const nextMatch = restContent.match(nextMilestonePattern); + + let sectionEnd; + if (nextMatch) { + sectionEnd = sectionStart + sectionMatch[0].length + nextMatch.index; + } else { + sectionEnd = content.length; + } + + // Return everything before the current milestone section (non-milestone content + // like title, overview) plus the current milestone section + const beforeMilestones = content.slice(0, sectionStart); + const currentSection = content.slice(sectionStart, sectionEnd); + + // Also include any content before the first milestone heading (title, overview, etc.) + // but strip any
blocks in it (these are definitely shipped) + const preamble = beforeMilestones.replace(/
[\s\S]*?<\/details>/gi, ''); + + return preamble + currentSection; +} + /** * Replace a pattern only in the current milestone section of ROADMAP.md * (everything after the last
close tag). Used for write operations @@ -444,7 +529,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); @@ -549,7 +634,7 @@ 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) { @@ -597,6 +682,7 @@ module.exports = { getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, + extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..9c9d35655 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -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) { @@ -633,8 +633,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; diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d88be9459..6bf2e5cf5 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -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 @@ -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') ); @@ -824,7 +824,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { // 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) { diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 3164b702c..a199a6ab8 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -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 diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 9f8a08546..61fbc16c6 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, output, error } = require('./core.cjs'); +const { safeReadFile, 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(); @@ -695,7 +695,7 @@ function cmdValidateHealth(cwd, options, raw) { // 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; From c2c4301a98e83de60fe77449b7377e8b1454ab67 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:42 -0400 Subject: [PATCH 12/43] feat: model alias-to-full-ID resolution for Task API compatibility (#991) (#1141) Claude Code's Task tool sometimes doesn't resolve short aliases (opus, sonnet, haiku) and passes them directly to the API, causing 404s. Tasks then inherit the parent session's model instead of the configured one. Added: - MODEL_ALIAS_MAP in core.cjs mapping aliases to full model IDs - resolve_model_ids config option (default: false for backward compat) - resolveModelInternal() maps aliases when resolve_model_ids is true Usage: { "resolve_model_ids": true } This causes gsd-tools resolve-model to return 'claude-sonnet-4-5' instead of 'sonnet', which the Task tool passes to the API without needing alias resolution on Claude Code's side. The alias map is maintained per release. Users can also use model_overrides for full control. All 755 tests pass. Fixes #991 --- get-shit-done/bin/lib/core.cjs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 16d6e2726..5038dc247 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -64,6 +64,7 @@ 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 }; try { @@ -106,6 +107,7 @@ 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, model_overrides: parsed.model_overrides || null, }; } catch { @@ -557,6 +559,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); @@ -571,7 +586,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 ─────────────────────────────────────────────────────────── @@ -685,4 +708,5 @@ module.exports = { extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, + MODEL_ALIAS_MAP, }; From 309d8671723c3aa54edaba9cd35c4e99708f0a18 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:59 -0400 Subject: [PATCH 13/43] fix: prevent nested Skill calls that break AskUserQuestion (#1009) (#1140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When plan-phase invokes discuss-phase as a nested Skill call, AskUserQuestion calls auto-resolve with empty answers — the user never sees the question UI. This is a Claude Code runtime bug with nested subcontexts. Made the 'Run discuss-phase first' path explicitly exit the workflow with a display message instead of risking nested invocation: - Added explicit warning: do NOT invoke as nested Skill/Task - Show the command for user to run as top-level - Exit the plan-phase workflow immediately Fixes #1009 --- get-shit-done/workflows/plan-phase.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index f7114c5a8..8ec87113a 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -170,7 +170,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 From 6536214a3ace443b61aab27c271a83474cacce43 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:08 -0400 Subject: [PATCH 14/43] fix: add explicit agent type listings to prevent fallback after /clear (#949) (#1139) After /clear, Claude Code sometimes loses awareness of custom agent types and falls back to 'general-purpose'. This happens because the model doesn't re-read .claude/agents/ after context reset. Added sections to: - execute-phase.md: lists all 12 valid GSD agent types with descriptions - plan-phase.md: lists the 3 agent types used during planning The explicit listing in workflow instructions ensures the model always has an unambiguous reference to valid agent types, regardless of whether .claude/agents/ was re-read after /clear. Fixes #949 --- get-shit-done/workflows/execute-phase.md | 18 ++++++++++++++++++ get-shit-done/workflows/plan-phase.md | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index af9027722..b522dba55 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -10,6 +10,24 @@ Orchestrator coordinates, not executes. Each subagent loads the full execute-pla Read STATE.md before any operation to load project context. + +These are the valid GSD subagent types registered in .claude/agents/ (or equivalent for your runtime). +Always use the exact name from this list — do not fall back to 'general-purpose' or other built-in types: + +- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md +- gsd-verifier — Verifies phase completion, checks quality gates +- gsd-planner — Creates detailed plans from phase scope +- gsd-phase-researcher — Researches technical approaches for a phase +- gsd-plan-checker — Reviews plan quality before execution +- gsd-debugger — Diagnoses and fixes issues +- gsd-codebase-mapper — Maps project structure and dependencies +- gsd-integration-checker — Checks cross-phase integration +- gsd-nyquist-auditor — Validates verification coverage +- gsd-ui-researcher — Researches UI/UX approaches +- gsd-ui-checker — Reviews UI implementation quality +- gsd-ui-auditor — Audits UI against design requirements + + diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 8ec87113a..62815be58 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -8,6 +8,13 @@ Read all files referenced by the invoking prompt's execution_context before star @~/.claude/get-shit-done/references/ui-brand.md + +Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'): +- gsd-phase-researcher — Researches technical approaches for a phase +- gsd-planner — Creates detailed plans from phase scope +- gsd-plan-checker — Reviews plan quality before execution + + ## 1. Initialize From aa9cb7bcb6aca1453f80deb7a440897b88e1092e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:15 -0400 Subject: [PATCH 15/43] feat: add Codex hooks support for SessionStart (#1020) (#1138) Codex v0.114.0 added experimental hooks with SessionStart and Stop events. GSD's Codex installer now configures hooks in config.toml: - Enables codex_hooks feature flag in [features] section - Adds SessionStart hook for GSD update checking (gsd-update-check.js) - Graceful fallback if config.toml write fails Hook format follows Codex's TOML convention: [[hooks]] event = "SessionStart" command = "node /path/to/gsd-update-check.js" PostToolUse hooks (context monitor) are not yet added since Codex only supports SessionStart and Stop events currently. Fixes #1020 --- bin/install.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bin/install.js b/bin/install.js index 95b50d7e9..c79642a02 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2693,6 +2693,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 }; } From 665c948c221fc7b3b13f98c0ee7cdd6c19aba513 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:36 -0400 Subject: [PATCH 16/43] feat: MCP tool awareness for GSD subagents (#973) (#1137) GSD executor agents ignore MCP tools (e.g. jCodeMunch) even when CLAUDE.md explicitly instructs their use. Agents default to Grep/Glob because those are explicitly referenced in workflow patterns. Added MCP tool instructions to: - execute-phase.md: section in executor agent prompt telling agents to prefer MCP tools over Grep/Glob when available - execute-plan.md: Step 2 in execute section with MCP tool fallback guidance Agents now: 1. Check if CLAUDE.md references MCP tools 2. Prefer MCP tools for code navigation when accessible 3. Fall back to Grep/Glob if MCP tools are not available Fixes #973 --- get-shit-done/workflows/execute-phase.md | 7 +++++++ get-shit-done/workflows/execute-plan.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index b522dba55..f77f3f272 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -158,6 +158,13 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation) + + If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7, + or other MCP servers), prefer those tools over Grep/Glob for code navigation when available. + MCP tools often save significant tokens by providing structured code indexes. + Check tool availability first — if MCP tools are not accessible, fall back to Grep/Glob. + + - [ ] All tasks executed - [ ] Each task committed individually diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index d47b6a18f..5dd6f8fc0 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -135,7 +135,8 @@ If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness Deviations are normal — handle via rules below. 1. Read @context files from prompt -2. Per task: +2. **MCP tools:** If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch for code navigation), prefer them over Grep/Glob when available. Fall back to Grep/Glob if MCP tools are not accessible. +3. Per task: - **MANDATORY read_first gate:** If the task has a `` field, you MUST read every listed file BEFORE making any edits. This is not optional. Do not skip files because you "already know" what's in them — read them. The read_first files establish ground truth for the task. - `type="auto"`: if `tdd="true"` → TDD execution. Implement with deviation rules + auth gates. Verify done criteria. Commit (see task_commit). Track hash for Summary. - `type="checkpoint:*"`: STOP → checkpoint_protocol → wait for user → continue only after confirmation. From 27e9bad203f4345e2f392d9b4068faa7ce12bffa Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:59:11 -0400 Subject: [PATCH 17/43] feat: interactive executor mode for pair-programming style execution (#963) (#1136) Adds --interactive flag to /gsd:execute-phase that changes execution from autonomous subagent delegation to sequential inline execution with user checkpoints between tasks. Interactive mode: - Executes plans sequentially inline (no subagent spawning) - Presents each plan to user: Execute, Review first, Skip, Stop - Pauses after each task for user intervention - Dramatically lower token usage (no subagent overhead) - Maintains full GSD planning/tracking structure Changes: - execute-phase.md: new check_interactive_mode step with full interactive flow - execute-phase command: documented --interactive flag in argument-hint and context Use cases: - Small phases (1-3 plans, no complex dependencies) - Bug fixes and verification gap closure - Learning GSD workflow - When user wants to pair-program with Claude under GSD structure Fixes #963 --- commands/gsd/execute-phase.md | 3 +- get-shit-done/workflows/execute-phase.md | 48 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/commands/gsd/execute-phase.md b/commands/gsd/execute-phase.md index 1a798471f..b0741db0c 100644 --- a/commands/gsd/execute-phase.md +++ b/commands/gsd/execute-phase.md @@ -1,7 +1,7 @@ --- name: gsd:execute-phase description: Execute all plans in a phase with wave-based parallelization -argument-hint: " [--gaps-only]" +argument-hint: " [--gaps-only] [--interactive]" allowed-tools: - Read - Write @@ -31,6 +31,7 @@ Phase: $ARGUMENTS **Flags:** - `--gaps-only` — Execute only gap closure plans (plans with `gap_closure: true` in frontmatter). Use after verify-work creates fix plans. +- `--interactive` — Execute plans sequentially inline (no subagents) with user checkpoints between tasks. Lower token usage, pair-programming style. Best for small phases, bug fixes, and verification gaps. Context files are resolved inside the workflow via `gsd-tools init execute-phase` and per-subagent `` blocks. diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index f77f3f272..9e36e30f5 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -55,6 +55,54 @@ fi ``` + +**Parse `--interactive` flag from $ARGUMENTS.** + +**If `--interactive` flag present:** Switch to interactive execution mode. + +Interactive mode executes plans sequentially **inline** (no subagent spawning) with user +checkpoints between tasks. The user can review, modify, or redirect work at any point. + +**Interactive execution flow:** + +1. Load plan inventory as normal (discover_and_group_plans) +2. For each plan (sequentially, ignoring wave grouping): + + a. **Present the plan to the user:** + ``` + ## Plan {plan_id}: {plan_name} + + Objective: {from plan file} + Tasks: {task_count} + + Options: + - Execute (proceed with all tasks) + - Review first (show task breakdown before starting) + - Skip (move to next plan) + - Stop (end execution, save progress) + ``` + + b. **If "Review first":** Read and display the full plan file. Ask again: Execute, Modify, Skip. + + c. **If "Execute":** Read and follow `~/.claude/get-shit-done/workflows/execute-plan.md` **inline** + (do NOT spawn a subagent). Execute tasks one at a time. + + d. **After each task:** Pause briefly. If the user intervenes (types anything), stop and address + their feedback before continuing. Otherwise proceed to next task. + + e. **After plan complete:** Show results, commit, create SUMMARY.md, then present next plan. + +3. After all plans: proceed to verification (same as normal mode). + +**Benefits of interactive mode:** +- No subagent overhead — dramatically lower token usage +- User catches mistakes early — saves costly verification cycles +- Maintains GSD's planning/tracking structure +- Best for: small phases, bug fixes, verification gaps, learning GSD + +**Skip to handle_branching step** (interactive plans execute inline after grouping). + + Check `branching_strategy` from init: From 7dd31e6d202cfde647b90b847033c65907f687c0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:59:39 -0400 Subject: [PATCH 18/43] =?UTF-8?q?feat:=20signal=20file=20for=20decision=20?= =?UTF-8?q?points=20=E2=80=94=20WAITING.json=20(#1034)=20(#1133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GSD hits a blocking decision point (AskUserQuestion, next action prompt), external watchers have no way to detect it. Users monitoring multiple auto sessions must visually check each terminal. Added: - state signal-waiting: writes .planning/WAITING.json (or .gsd/WAITING.json) with type, question, options, timestamp, and phase info - state signal-resume: removes WAITING.json when user answers Signal file format: { status, type, question, options[], since, phase } External tools can watch for this file via fswatch, polling, or inotify. Complements the existing remote-questions extension (Slack/Discord). Fixes #1034 --- get-shit-done/bin/gsd-tools.cjs | 17 ++++++++++++ get-shit-done/bin/lib/state.cjs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 16975e8f2..cdee42566 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -15,6 +15,8 @@ * state get [section] Get STATE.md content or section * state patch --field val ... Batch update STATE.md fields * state begin-phase --phase N --name S --plans C Update STATE.md for new phase start + * state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal + * state signal-resume Remove WAITING.json signal * resolve-model Get model for agent based on profile * find-phase Find phase directory by number * commit [--files f1 f2] Commit planning docs @@ -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); } diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..78797694a 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -778,6 +778,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 +843,6 @@ module.exports = { cmdStateSnapshot, cmdStateJson, cmdStateBeginPhase, + cmdSignalWaiting, + cmdSignalResume, }; From e8dbc3031bbbc809d9071fa3fa0aa2b1db2086c3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:07 -0400 Subject: [PATCH 19/43] fix: add runtime compatibility fallback for Copilot executor stuck issue (#1128) (#1131) Copilot's subagent spawning (Task API) may not properly return completion signals to the orchestrator, causing it to hang indefinitely waiting for agents that have already finished their work. Added section to execute-phase.md with: - Runtime-specific subagent spawning guidance (Claude Code, Copilot, others) - Fallback rule: if agent completes work but orchestrator doesn't get the signal, treat as success based on spot-checks (SUMMARY.md exists, commits present) - Sequential inline execution fallback for runtimes without reliable Task API Fixes #1128 --- get-shit-done/workflows/execute-phase.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 9e36e30f5..243103b9d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -6,6 +6,20 @@ Execute all plans in a phase using wave-based parallel execution. Orchestrator s Orchestrator coordinates, not executes. Each subagent loads the full execute-plan context. Orchestrator: discover plans → analyze deps → group waves → spawn agents → handle checkpoints → collect results. + +**Subagent spawning is runtime-specific:** +- **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result +- **Copilot:** 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. + + Read STATE.md before any operation to load project context. From 7101ddcb9c179900ccbb350b3ffecf742611f8ae Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:24 -0400 Subject: [PATCH 20/43] fix: prevent PROJECT.md drift and fix phase completion counters (#956) (#1130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the standard workflow cycle caused planning document drift: 1. PROJECT.md was never updated during discuss → plan → execute → verify. Only transition.md (optional) and complete-milestone evolved it. Added an 'update_project_md' step to execute-phase.md that evolves PROJECT.md after phase completion: moves requirements to Validated, updates Current State, bumps Last Updated timestamp. 2. cmdPhaseComplete() in phase.cjs advanced 'Current Phase' but never incremented 'Completed Phases' counter or recalculated 'percent'. Added counter increment and percentage recalculation based on completed/total phases ratio. Addresses the workflow-level gaps identified in #956: - PROJECT.md evolution in execute-phase (gap #2) - completed_phases counter not incremented (gap #1 table row 3) - percent never recalculated (gap #1 table row 4) Fixes #956 --- get-shit-done/bin/lib/phase.cjs | 28 ++++++++++++++++++++++++ get-shit-done/workflows/execute-phase.md | 22 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 6bf2e5cf5..5cb5eac4d 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -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); } diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 243103b9d..d70966162 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -499,6 +499,28 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): co ``` + +**Evolve PROJECT.md to reflect phase completion (prevents planning document drift — #956):** + +PROJECT.md tracks validated requirements, decisions, and current state. Without this step, +PROJECT.md falls behind silently over multiple phases. + +1. Read `.planning/PROJECT.md` +2. If the file exists and has a `## Validated Requirements` or `## Requirements` section: + - Move any requirements validated by this phase from Active → Validated + - Add a brief note: `Validated in Phase {X}: {Name}` +3. If the file has a `## Current State` or similar section: + - Update it to reflect this phase's completion (e.g., "Phase {X} complete — {one-liner}") +4. Update the `Last updated:` footer to today's date +5. Commit the change: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md +``` + +**Skip this step if** `.planning/PROJECT.md` does not exist. + + **Exception:** If `gaps_found`, the `verify_phase_goal` step already presents the gap-closure path (`/gsd:plan-phase {X} --gaps`). No additional routing needed — skip auto-advance. From 849aed665469c0cca5f20654bab47b762bcb13f6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:35 -0400 Subject: [PATCH 21/43] fix: prevent agent from suggesting non-existent /gsd:transition command (#1081) (#1129) The agent was telling users to run '/gsd:transition' after phase completion, but this command does not exist. transition.md is an internal workflow invoked by execute-phase during auto-advance. Changes: - Add banner to transition.md declaring it is NOT a user command - Add explicit warning in execute-phase completion section that /gsd:transition does not exist - Add 'only suggest commands listed above' guard to prevent hallucination - Update resume-project.md to avoid ambiguous 'Transition' label - Replace 'ready for transition' with 'ready for next step' in execute-plan.md Fixes #1081 --- .release-monitor.sh | 51 +++++++++++++++++++++++ get-shit-done/workflows/execute-phase.md | 4 ++ get-shit-done/workflows/execute-plan.md | 2 +- get-shit-done/workflows/resume-project.md | 4 +- get-shit-done/workflows/transition.md | 16 +++++++ 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100755 .release-monitor.sh diff --git a/.release-monitor.sh b/.release-monitor.sh new file mode 100755 index 000000000..200383398 --- /dev/null +++ b/.release-monitor.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Release monitor for gsd-build/get-shit-done +# Checks every 15 minutes, writes new release info to a signal file + +REPO="gsd-build/get-shit-done" +SIGNAL_FILE="/tmp/gsd-new-release.json" +STATE_FILE="/tmp/gsd-monitor-last-tag" +LOG_FILE="/tmp/gsd-monitor.log" + +# Initialize with current latest +echo "v1.25.1" > "$STATE_FILE" +rm -f "$SIGNAL_FILE" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log "Monitor started. Watching $REPO for releases newer than v1.25.1" +log "Checking every 15 minutes..." + +while true; do + sleep 900 # 15 minutes + + LAST_KNOWN=$(cat "$STATE_FILE" 2>/dev/null) + + # Get latest release tag + LATEST=$(gh release list -R "$REPO" --limit 1 2>/dev/null | awk '{print $1}') + + if [ -z "$LATEST" ]; then + log "WARNING: Failed to fetch releases (network issue?)" + continue + fi + + if [ "$LATEST" != "$LAST_KNOWN" ]; then + log "NEW RELEASE DETECTED: $LATEST (was: $LAST_KNOWN)" + + # Fetch release notes + RELEASE_BODY=$(gh release view "$LATEST" -R "$REPO" --json tagName,name,body 2>/dev/null) + + # Write signal file for the agent to pick up + echo "$RELEASE_BODY" > "$SIGNAL_FILE" + echo "$LATEST" > "$STATE_FILE" + + log "Signal file written to $SIGNAL_FILE" + # Exit so the agent can process it, then restart + exit 0 + else + log "No new release. Latest is still $LATEST" + fi +done diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index d70966162..90891516c 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -574,6 +574,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 @@ -582,6 +584,8 @@ 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. diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 5dd6f8fc0..e64dec825 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -371,7 +371,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not " Include: duration, start/end times, task count, file count. -Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition". +Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step". diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 00ce54df0..7ebdc2150 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -154,7 +154,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 +242,7 @@ Based on user selection, route to appropriate workflow: --- ``` -- **Transition** → ./transition.md +- **Advance to next phase** → ./transition.md (internal workflow, invoked inline — NOT a user command) - **Check todos** → Read .planning/todos/pending/, present summary - **Review alignment** → Read PROJECT.md, compare to current state - **Something else** → Ask what they need diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 5e8927dfd..d3073cb6a 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -1,3 +1,19 @@ + + +**This is an INTERNAL workflow — NOT a user-facing command.** + +There is no `/gsd:transition` command. This workflow is invoked automatically by +`execute-phase` during auto-advance, or inline by the orchestrator after phase +verification. Users should never be told to run `/gsd:transition`. + +**Valid user commands for phase progression:** +- `/gsd:discuss-phase {N}` — discuss a phase before planning +- `/gsd:plan-phase {N}` — plan a phase +- `/gsd:execute-phase {N}` — execute a phase +- `/gsd:progress` — see roadmap progress + + + **Read these files NOW:** From a97e4c2c6fc3640c02148aeb43c5ece4b8df612f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:01:08 -0400 Subject: [PATCH 22/43] feat: /gsd:ship command for PR creation from verified phase work (#829) (#1123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: /gsd:ship command for PR creation from verified phase work (#829) New command that bridges local completion → merged PR, closing the plan → execute → verify → ship loop. Workflow (workflows/ship.md): 1. Preflight: verification passed, clean tree, correct branch, gh auth 2. Push branch to remote 3. Auto-generate rich PR body from planning artifacts: - Phase goal from ROADMAP.md - Changes from SUMMARY.md files - Requirements addressed (REQ-IDs) - Verification status - Key decisions 4. Create PR via gh CLI (supports --draft) 5. Optional code review request 6. Update STATE.md with shipping status Files: - commands/gsd/ship.md: New command entry point - get-shit-done/workflows/ship.md: Full workflow implementation - get-shit-done/workflows/help.md: Add ship to help output - docs/COMMANDS.md: Command reference - docs/FEATURES.md: Feature spec with REQ-SHIP-01 through 05 - docs/USER-GUIDE.md: Add to command table - CHANGELOG.md: Document new command Fixes #829 * fix(tests): update expected skill count from 39 to 40 for new ship command The Copilot install E2E tests hardcode the expected number of skill directories and manifest entries. Adding commands/gsd/ship.md increased the count from 39 to 40. --- CHANGELOG.md | 1 + commands/gsd/ship.md | 23 ++++ docs/COMMANDS.md | 26 ++++ docs/FEATURES.md | 19 +++ docs/USER-GUIDE.md | 37 +++--- get-shit-done/workflows/help.md | 14 ++ get-shit-done/workflows/ship.md | 228 ++++++++++++++++++++++++++++++++ 7 files changed, 330 insertions(+), 18 deletions(-) create mode 100644 commands/gsd/ship.md create mode 100644 get-shit-done/workflows/ship.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f7d2a69..1eee1e152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - 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 +- **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) ### Fixed - **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948) diff --git a/commands/gsd/ship.md b/commands/gsd/ship.md new file mode 100644 index 000000000..124695553 --- /dev/null +++ b/commands/gsd/ship.md @@ -0,0 +1,23 @@ +--- +name: gsd:ship +description: Create PR, run review, and prepare for merge after verification passes +argument-hint: "[phase number or milestone, e.g., '4' or 'v1.0']" +allowed-tools: + - Read + - Bash + - Grep + - Glob + - Write + - AskUserQuestion +--- + +Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge. + +Closes the plan → execute → verify → ship loop. + + + +@~/.claude/get-shit-done/workflows/ship.md + + +Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..9b7947737 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -132,6 +132,32 @@ User acceptance testing with auto-diagnosis. --- +### `/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. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 27afed54e..88c543a1f 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -261,6 +261,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]` diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5458cedc0..d29455860 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -284,6 +284,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. | `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase | | `/gsd:execute-phase ` | Execute all plans in parallel waves | After planning is complete | | `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes | +| `/gsd:ship [N]` | Create PR from verified work | After verification passes | | `/gsd: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 | @@ -363,7 +364,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 +408,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 (for example OpenCode `/model`). --- diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 058d4a810..e32ea0d25 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -308,6 +308,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]`** diff --git a/get-shit-done/workflows/ship.md b/get-shit-done/workflows/ship.md new file mode 100644 index 000000000..3c29de1ff --- /dev/null +++ b/get-shit-done/workflows/ship.md @@ -0,0 +1,228 @@ + +Create a pull request from completed phase/milestone work, generate a rich PR body from planning artifacts, optionally run code review, and prepare for merge. Closes the plan → execute → verify → ship loop. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +Parse arguments and load project state: + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}") +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +``` + +Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`. + +Also load config for branching strategy: +```bash +CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) +``` + +Extract: `branching_strategy`, `branch_name`. + + + +Verify the work is ready to ship: + +1. **Verification passed?** + ```bash + VERIFICATION=$(cat ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null) + ``` + Check for `status: passed` or `status: human_needed` (with human approval). + If no VERIFICATION.md or status is `gaps_found`: warn and ask user to confirm. + +2. **Clean working tree?** + ```bash + git status --short + ``` + If uncommitted changes exist: ask user to commit or stash first. + +3. **On correct branch?** + ```bash + CURRENT_BRANCH=$(git branch --show-current) + ``` + If on `main`/`master`: warn — should be on a feature branch. + If branching_strategy is `none`: offer to create a branch now. + +4. **Remote configured?** + ```bash + git remote -v | head -2 + ``` + Detect `origin` remote. If no remote: error — can't create PR. + +5. **`gh` CLI available?** + ```bash + which gh && gh auth status 2>&1 + ``` + If `gh` not found or not authenticated: provide setup instructions and exit. + + + +Push the current branch to remote: + +```bash +git push origin ${CURRENT_BRANCH} 2>&1 +``` + +If push fails (e.g., no upstream): set upstream: +```bash +git push --set-upstream origin ${CURRENT_BRANCH} 2>&1 +``` + +Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)" + + + +Auto-generate a rich PR body from planning artifacts: + +**1. Title:** +``` +Phase {phase_number}: {phase_name} +``` +Or for milestone: `Milestone {version}: {name}` + +**2. Summary section:** +Read ROADMAP.md for phase goal. Read VERIFICATION.md for verification status. + +```markdown +## Summary + +**Phase {N}: {Name}** +**Goal:** {goal from ROADMAP.md} +**Status:** Verified ✓ + +{One paragraph synthesized from SUMMARY.md files — what was built} +``` + +**3. Changes section:** +For each SUMMARY.md in the phase directory: +```markdown +## Changes + +### Plan {plan_id}: {plan_name} +{one_liner from SUMMARY.md frontmatter} + +**Key files:** +{key-files.created and key-files.modified from SUMMARY.md frontmatter} +``` + +**4. Requirements section:** +```markdown +## Requirements Addressed + +{REQ-IDs from plan frontmatter, linked to REQUIREMENTS.md descriptions} +``` + +**5. Testing section:** +```markdown +## Verification + +- [x] Automated verification: {pass/fail from VERIFICATION.md} +- {human verification items from VERIFICATION.md, if any} +``` + +**6. Decisions section:** +```markdown +## Key Decisions + +{Decisions from STATE.md accumulated context relevant to this phase} +``` + + + +Create the PR using the generated body: + +```bash +gh pr create \ + --title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \ + --body "${PR_BODY}" \ + --base main +``` + +If `--draft` flag was passed: add `--draft`. + +Report: "PR #{number} created: {url}" + + + +Ask if user wants to trigger a code review: + +``` +AskUserQuestion: + question: "PR created. Run a code review before merge?" + options: + - label: "Skip review" + description: "PR is ready — merge when CI passes" + - label: "Self-review" + description: "I'll review the diff in the PR myself" + - label: "Request review" + description: "Request review from a teammate" +``` + +**If "Request review":** +```bash +gh pr edit ${PR_NUMBER} --add-reviewer "${REVIEWER}" +``` + +**If "Self-review":** +Report the PR URL and suggest: "Review the diff at {url}/files" + + + +Update STATE.md to reflect the shipping action: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)" +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}" +``` + +If `commit_docs` is true: +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md +``` + + + +``` +─────────────────────────────────────────────────────────────── + +## ✓ Phase {X}: {Name} — Shipped + +PR: #{number} ({url}) +Branch: {branch} → main +Commits: {count} +Verification: ✓ Passed +Requirements: {N} REQ-IDs addressed + +Next steps: +- Review/approve PR +- Merge when CI passes +- /gsd:complete-milestone (if last phase in milestone) +- /gsd:progress (to see what's next) + +─────────────────────────────────────────────────────────────── +``` + + + + + +After shipping: + +- /gsd:complete-milestone — if all phases in milestone are done +- /gsd:progress — see overall project state +- /gsd:execute-phase {next} — continue to next phase + + + +- [ ] Preflight checks passed (verification, clean tree, branch, remote, gh) +- [ ] Branch pushed to remote +- [ ] PR created with rich auto-generated body +- [ ] STATE.md updated with shipping status +- [ ] User knows PR number and next steps + From f54f3df77685f9acc10bbfa56e6d5cdbfce6d7cb Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:01:25 -0400 Subject: [PATCH 23/43] feat: structured session handoff artifact for cross-session continuity (#940) (#1122) Enhance /gsd:pause-work to write .planning/HANDOFF.json alongside .continue-here.md. The JSON provides machine-readable state that /gsd:resume-work can parse for precise resumption. HANDOFF.json includes: - Task position (phase, plan, task number, status) - Completed and remaining tasks with commit hashes - Blockers with type classification (technical/human_action/external) - Human actions pending (API keys, approvals, manual testing) - Uncommitted files list - Context notes for mental model restoration Resume-work changes: - HANDOFF.json is primary resumption source (highest priority) - Surfaces blockers and human actions immediately on session start - Validates uncommitted files against git status - Deletes HANDOFF.json after successful resumption - Falls back to .continue-here.md if no JSON exists Also checks for placeholder content in SUMMARY.md files to catch false completions (frontmatter claims complete but body has TBD). Fixes #940 --- CHANGELOG.md | 3 ++ docs/FEATURES.md | 6 ++- docs/USER-GUIDE.md | 2 +- get-shit-done/workflows/pause-work.md | 64 +++++++++++++++++++++-- get-shit-done/workflows/resume-project.md | 22 +++++++- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eee1e152..6f1947b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948) +### Improved +- **Structured session handoff** — `/gsd:pause-work` now writes `.planning/HANDOFF.json` alongside `.continue-here.md`. JSON provides machine-readable state (task position, blockers, human actions pending, uncommitted files) that `/gsd:resume-work` parses for precise resumption instead of generic "what do you want to do?" (#940) + ## [1.25.0] - 2026-03-16 ### Added diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 88c543a1f..b706e257d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -513,11 +513,13 @@ **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 --- diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index d29455860..4dcfca52b 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -296,7 +296,7 @@ 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: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 | diff --git a/get-shit-done/workflows/pause-work.md b/get-shit-done/workflows/pause-work.md index f723ef81a..ccdba267d 100644 --- a/get-shit-done/workflows/pause-work.md +++ b/get-shit-done/workflows/pause-work.md @@ -1,5 +1,5 @@ -Create `.continue-here.md` handoff file to preserve complete work state across sessions. Enables seamless resumption with full context restoration. +Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context. @@ -27,10 +27,61 @@ If no active phase detected, ask user which phase they're pausing work on. 3. **Work remaining**: What's left in current plan/phase 4. **Decisions made**: Key decisions and rationale 5. **Blockers/issues**: Anything stuck -6. **Mental context**: The approach, next steps, "vibe" -7. **Files modified**: What's changed but not committed +6. **Human actions pending**: Things that need manual intervention (MCP setup, API keys, approvals, manual testing) +7. **Background processes**: Any running servers/watchers that were part of the workflow +8. **Files modified**: What's changed but not committed Ask user for clarifications if needed via conversational questions. + +**Also inspect SUMMARY.md files for false completions:** +```bash +# Check for placeholder content in existing summaries +grep -l "To be filled\|placeholder\|TBD" .planning/phases/*/*.md 2>/dev/null +``` +Report any summaries with placeholder content as incomplete items. + + + +**Write structured handoff to `.planning/HANDOFF.json`:** + +```bash +timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timestamp full --raw) +``` + +```json +{ + "version": "1.0", + "timestamp": "{timestamp}", + "phase": "{phase_number}", + "phase_name": "{phase_name}", + "phase_dir": "{phase_dir}", + "plan": {current_plan_number}, + "task": {current_task_number}, + "total_tasks": {total_task_count}, + "status": "paused", + "completed_tasks": [ + {"id": 1, "name": "{task_name}", "status": "done", "commit": "{short_hash}"}, + {"id": 2, "name": "{task_name}", "status": "done", "commit": "{short_hash}"}, + {"id": 3, "name": "{task_name}", "status": "in_progress", "progress": "{what_done}"} + ], + "remaining_tasks": [ + {"id": 4, "name": "{task_name}", "status": "not_started"}, + {"id": 5, "name": "{task_name}", "status": "not_started"} + ], + "blockers": [ + {"description": "{blocker}", "type": "technical|human_action|external", "workaround": "{if any}"} + ], + "human_actions_pending": [ + {"action": "{what needs to be done}", "context": "{why}", "blocking": true} + ], + "decisions": [ + {"decision": "{what}", "rationale": "{why}", "phase": "{phase_number}"} + ], + "uncommitted_files": [], + "next_action": "{specific first action when resuming}", + "context_notes": "{mental state, approach, what you were thinking}" +} +``` @@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md .planning/HANDOFF.json ``` ``` -✓ Handoff created: .planning/phases/[XX-name]/.continue-here.md +✓ Handoff created: + - .planning/HANDOFF.json (structured, machine-readable) + - .planning/phases/[XX-name]/.continue-here.md (human-readable) Current state: - Phase: [XX-name] - Task: [X] of [Y] - Status: [in_progress/blocked] +- Blockers: [count] ({human_actions_pending count} need human action) - Committed as WIP To resume: /gsd:resume-work diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 7ebdc2150..a8dafcf2c 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -63,6 +63,9 @@ cat .planning/PROJECT.md Look for incomplete work that needs attention: ```bash +# Check for structured handoff (preferred — machine-readable) +cat .planning/HANDOFF.json 2>/dev/null + # Check for continue-here files (mid-plan resumption) ls .planning/phases/*/.continue-here*.md 2>/dev/null @@ -78,7 +81,18 @@ if [ "$has_interrupted_agent" = "true" ]; then fi ``` -**If .continue-here file exists:** +**If HANDOFF.json exists:** + +- This is the primary resumption source — structured data from `/gsd:pause-work` +- Parse `status`, `phase`, `plan`, `task`, `total_tasks`, `next_action` +- Check `blockers` and `human_actions_pending` — surface these immediately +- Check `completed_tasks` for `in_progress` items — these need attention first +- Validate `uncommitted_files` against `git status` — flag divergence +- Use `context_notes` to restore mental model +- Flag: "Found structured handoff — resuming from task {task}/{total_tasks}" +- **After successful resumption, delete HANDOFF.json** (it's a one-shot artifact) + +**If .continue-here file exists (fallback):** - This is a mid-plan resumption point - Read the file for specific resumption context @@ -145,8 +159,12 @@ Based on project state, determine the most logical next action: → Primary: Resume interrupted agent (Task tool with resume parameter) → Option: Start fresh (abandon agent work) +**If HANDOFF.json exists:** +→ Primary: Resume from structured handoff (highest priority — specific task/blocker context) +→ Option: Discard handoff and reassess from files + **If .continue-here file exists:** -→ Primary: Resume from checkpoint +→ Fallback: Resume from checkpoint → Option: Start fresh on current plan **If incomplete plan (PLAN without SUMMARY):** From 0f095ac3d0389925787124c12033bdfc3c9b7fca Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:02:02 -0400 Subject: [PATCH 24/43] feat: requirements coverage gate in plan-phase pipeline (#984) (#1121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add step 13 (Requirements Coverage Gate) to plan-phase workflow. After plans pass the checker, verifies all phase requirements are covered by at least one plan before declaring planning complete. - Extracts REQ-IDs from plan frontmatter and compares against phase_req_ids from ROADMAP - Cross-checks CONTEXT.md features against plan objectives to detect silently dropped scope - Reports gaps with options: re-plan, defer, or proceed - Skips when phase_req_ids is null/TBD (no requirements mapped) Fixes #984 Co-authored-by: TÂCHES --- CHANGELOG.md | 1 + docs/FEATURES.md | 1 + get-shit-done/workflows/plan-phase.md | 55 ++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1947b3f..0bc4b9986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - 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 +- **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) - **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) ### Fixed diff --git a/docs/FEATURES.md b/docs/FEATURES.md index b706e257d..7ca07e297 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -171,6 +171,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 | diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 62815be58..2d3291533 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -587,11 +587,62 @@ Display: `Max iterations reached. {N} issues remain:` + issue list Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon -## 13. Present Final Status +## 13. Requirements Coverage Gate + +After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan. + +**Skip if:** `phase_req_ids` is null or TBD (no requirements mapped to this phase). + +**Step 1: Extract requirement IDs claimed by plans** +```bash +# Collect all requirement IDs from plan frontmatter +PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u) +``` + +**Step 2: Compare against phase requirements from ROADMAP** + +For each REQ-ID in `phase_req_ids`: +- If REQ-ID appears in `PLAN_REQS` → covered ✓ +- If REQ-ID does NOT appear in any plan → uncovered ✗ + +**Step 3: Check CONTEXT.md features against plan objectives** + +Read CONTEXT.md `` section. Extract feature/capability names. Check each against plan `` blocks. Features not mentioned in any plan objective → potentially dropped. + +**Step 4: Report** + +If all requirements covered and no dropped features: +``` +✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans +``` +→ Proceed to step 14. + +If gaps found: +``` +## ⚠ Requirements Coverage Gap + +{M} of {N} phase requirements are not assigned to any plan: + +| REQ-ID | Description | Plans | +|--------|-------------|-------| +| {id} | {from REQUIREMENTS.md} | None | + +{K} CONTEXT.md features not found in plan objectives: +- {feature_name} — described in CONTEXT.md but no plan covers it + +Options: +1. Re-plan to include missing requirements (recommended) +2. Move uncovered requirements to next phase +3. Proceed anyway — accept coverage gaps +``` + +Use AskUserQuestion to present the options. + +## 14. Present Final Status Route to `` OR `auto_advance` depending on flags/config. -## 14. Auto-Advance Check +## 15. Auto-Advance Check Check for auto-advance trigger: From a75c1d1f671af2732990bf040735b30426de4236 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:02:34 -0400 Subject: [PATCH 25/43] feat: cross-phase regression gate in execute-phase pipeline (#945) (#1120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add regression_gate step between executor completion and verification in execute-phase workflow. Runs prior phases' test suites to catch cross-phase regressions before they compound. - Discovers prior VERIFICATION.md files and extracts test file paths - Detects project test runner (jest/vitest/cargo/pytest) - Reports pass/fail with options to fix, continue, or abort - Skips silently for first phase or when no prior tests exist Changes: - execute-phase.md: New regression_gate step - CHANGELOG.md: Document regression gate feature - docs/FEATURES.md: Add REQ-EXEC-09 Fixes #945 Co-authored-by: TÂCHES --- CHANGELOG.md | 1 + docs/FEATURES.md | 1 + get-shit-done/workflows/execute-phase.md | 61 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc4b9986..cb350eb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - 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 +- **Cross-phase regression gate** — New `regression_gate` step in `execute-phase` runs prior phases' test suites after execution completes but before verification, catching regressions before they compound (#945) - **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) - **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 7ca07e297..79e4a83d9 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -221,6 +221,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 | diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 90891516c..e5c6afe4d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -415,6 +415,67 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PARENT ``` + +Run prior phases' test suites to catch cross-phase regressions BEFORE verification. + +**Skip if:** This is the first phase (no prior phases), or no prior VERIFICATION.md files exist. + +**Step 1: Discover prior phases' test files** +```bash +# Find all VERIFICATION.md files from prior phases in current milestone +PRIOR_VERIFICATIONS=$(find .planning/phases/ -name "*-VERIFICATION.md" ! -path "*${PHASE_NUMBER}*" 2>/dev/null) +``` + +**Step 2: Extract test file lists from prior verifications** + +For each VERIFICATION.md found, look for test file references: +- Lines containing `test`, `spec`, or `__tests__` paths +- The "Test Suite" or "Automated Checks" section +- File patterns from `key-files.created` in corresponding SUMMARY.md files that match `*.test.*` or `*.spec.*` + +Collect all unique test file paths into `REGRESSION_FILES`. + +**Step 3: Run regression tests (if any found)** + +```bash +# Detect test runner and run prior phase tests +if [ -f "package.json" ]; then + # Node.js — use project's test runner + npx jest ${REGRESSION_FILES} --passWithNoTests --no-coverage -q 2>&1 || npx vitest run ${REGRESSION_FILES} 2>&1 +elif [ -f "Cargo.toml" ]; then + cargo test 2>&1 +elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then + python -m pytest ${REGRESSION_FILES} -q --tb=short 2>&1 +fi +``` + +**Step 4: Report results** + +If all tests pass: +``` +✓ Regression gate: {N} prior-phase test files passed — no regressions detected +``` +→ Proceed to verify_phase_goal + +If any tests fail: +``` +## ⚠ Cross-Phase Regression Detected + +Phase {X} execution may have broken functionality from prior phases. + +| Test File | Phase | Status | Detail | +|-----------|-------|--------|--------| +| {file} | {origin_phase} | FAILED | {first_failure_line} | + +Options: +1. Fix regressions before verification (recommended) +2. Continue to verification anyway (regressions will compound) +3. Abort phase — roll back and re-plan +``` + +Use AskUserQuestion to present the options. + + Verify phase achieved its GOAL, not just completed tasks. From 9bf78719b66b6868eb1a26d814160c58b350a3b0 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:08:49 -0600 Subject: [PATCH 26/43] docs: update changelog for v1.26.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb350eb5a..43618437d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +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 -- **Cross-phase regression gate** — New `regression_gate` step in `execute-phase` runs prior phases' test suites after execution completes but before verification, catching regressions before they compound (#945) -- **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) -- **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) +- **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) - -### Improved -- **Structured session handoff** — `/gsd:pause-work` now writes `.planning/HANDOFF.json` alongside `.continue-here.md`. JSON provides machine-readable state (task position, blockers, human actions pending, uncommitted files) that `/gsd:resume-work` parses for precise resumption instead of generic "what do you want to do?" (#940) +- 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 @@ -1542,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 From 641a4fc15af147c1ebdbfd2f8a8b382a072855c0 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:08:52 -0600 Subject: [PATCH 27/43] 1.26.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ffa43783..2e1af42c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "license": "MIT", "bin": { "get-shit-done-cc": "bin/install.js" diff --git a/package.json b/package.json index a03f6c366..5a11cfb12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.", "bin": { "get-shit-done-cc": "bin/install.js" From fc468adb4260dc76dc308e2aba969ff42f09c702 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:16:26 -0600 Subject: [PATCH 28/43] fix(tests): update copilot skill count assertions for ship and next commands Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/copilot-install.test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 88404019e..ea79e411d 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 40, `expected 40 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 42, `expected 42 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 = 40; +const EXPECTED_SKILLS = 42; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 0f112abf55e71f0201d713361a24e9763694343e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 20:31:17 -0400 Subject: [PATCH 29/43] fix: remove model:inherit from OpenCode agent conversion (#1156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode does not recognize 'model: inherit' as a valid model identifier — it throws ProviderModelNotFoundError when spawning any GSD subagent. Changes: - Remove 'model: inherit' injection for OpenCode agents in install.js - Strip 'model:' field entirely during OpenCode frontmatter conversion (OpenCode uses its configured default model when no model is specified) - Update tests to verify model: inherit is NOT added This fixes all 15 GSD agent definitions and the gsd-set-profile command for OpenCode users. Fixes #1156 --- bin/install.js | 11 ++++++++++- tests/runtime-converters.test.cjs | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index c79642a02..819e531e5 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1228,6 +1228,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 +1271,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'); } diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 6491d9376..5da337310 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -5,6 +5,8 @@ * Larger runtime test suites (Copilot, Codex, Antigravity) have their own files. * * OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes) + * model: inherit is NOT added (OpenCode doesn't support it — see #1156) + * but mode: subagent IS added (required by OpenCode agents). * Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping) */ @@ -56,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', () => { From e84999663cd955fb87ade79dc0065d5d1abf874d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 16:51:24 -0400 Subject: [PATCH 30/43] feat(discuss-phase): cross-reference pending todos against phase scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a cross_reference_todos step to discuss-phase that surfaces relevant backlog items before scope-setting decisions are made. Implementation: - New 'todo match-phase ' CLI command (commands.cjs) that scores pending todos against a phase's ROADMAP goal using three heuristics: keyword overlap, area match, and file path overlap - New cross_reference_todos step in discuss-phase.md between load_prior_context and scout_codebase - CONTEXT.md template gains 'Folded Todos' subsection in and 'Reviewed Todos (not folded)' subsection in Design: - No AI call for matching — pure keyword/area/file heuristics for speed - Silent skip when todo_count is 0 or no matches (no workflow slowdown) - Auto mode folds all todos with score >= 0.4 automatically - Scoring: keywords (up to 0.6), area match (0.3), file overlap (0.4) Tests: 5 new tests covering empty state, keyword matching, unrelated todo exclusion, area matching, and score sorting. Closes #1111 --- get-shit-done/bin/gsd-tools.cjs | 4 +- get-shit-done/bin/lib/commands.cjs | 127 ++++++++++++++++++++++- get-shit-done/workflows/discuss-phase.md | 52 ++++++++++ tests/commands.test.cjs | 92 ++++++++++++++++ 4 files changed, 273 insertions(+), 2 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index cdee42566..558185ac5 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -534,8 +534,10 @@ async function main() { const subcommand = args[1]; if (subcommand === 'complete') { commands.cmdTodoComplete(cwd, args[2], raw); + } else if (subcommand === 'match-phase') { + commands.cmdTodoMatchPhase(cwd, args[2], raw); } else { - error('Unknown todo subcommand. Available: complete'); + error('Unknown todo subcommand. Available: complete, match-phase'); } break; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..ff13ad6cb 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, 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'); @@ -448,6 +448,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'); @@ -704,6 +828,7 @@ module.exports = { cmdWebsearch, cmdProgressRender, cmdTodoComplete, + cmdTodoMatchPhase, cmdScaffold, cmdStats, }; diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index d8f7a6f40..d21c1fe4a 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -242,6 +242,47 @@ Structure the extracted information: **If no prior context exists:** Continue without — this is expected for early phases. + +Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed. + +**Load and match todos:** +```bash +TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}") +``` + +Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`). + +**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown. + +**If matches found:** + +Present matched todos to the user. Show each match with its title, area, and why it matched: + +``` +📋 Found {N} pending todo(s) that may be relevant to Phase {X}: + +{For each match:} +- **{title}** (area: {area}, relevance: {score}) — matched on {reasons} +``` + +Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope: + +``` +Which of these todos should be folded into Phase {X} scope? +(Select any that apply, or none to skip) +``` + +**For selected (folded) todos:** +- Store internally as `` for inclusion in CONTEXT.md `` section +- These become additional scope items that downstream agents (researcher, planner) will see + +**For unselected (reviewed but not folded) todos:** +- Store internally as `` for inclusion in CONTEXT.md `` section +- This prevents future phases from re-surfacing the same todos as "missed" + +**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection. + + Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session. @@ -544,6 +585,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}" ### Claude's Discretion [Areas where user said "you decide" — note that Claude has flexibility here] +### Folded Todos +[If any todos were folded into scope from the cross_reference_todos step, list them here. +Each entry should include the todo title, original problem, and how it fits this phase's scope. +If no todos were folded: omit this subsection entirely.] + @@ -595,6 +641,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"] diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index b43d2827a..f84624e47 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -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 // ───────────────────────────────────────────────────────────────────────────── From 6e612c54d7428ed4920d3a9fe579048afa18c001 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:55:22 -0400 Subject: [PATCH 31/43] fix: embed evolution rules in generated PROJECT.md so agents see them at runtime (#1039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block in templates/project.md defined requirement lifecycle rules (validate, invalidate, add, log decisions) but these instructions only existed in the template — they never made it into the generated PROJECT.md that agents actually read during phase transitions. Changes: - new-project.md: Add ## Evolution section to generated PROJECT.md with the phase transition and milestone review checklists - new-milestone.md: Ensure ## Evolution section exists in PROJECT.md (backfills for projects created before this feature) - execute-phase.md: Add .planning/PROJECT.md to executor so executors have project context (core value, requirements, evolution) - templates/project.md: Add comment noting the block is implemented by transition.md and complete-milestone.md - docs/ARCHITECTURE.md, docs/FEATURES.md: Note evolution rules in PROJECT.md descriptions - CHANGELOG.md: Document the new Evolution section and executor context Fixes #1039 All 755 tests pass. --- docs/ARCHITECTURE.md | 2 +- docs/FEATURES.md | 2 +- get-shit-done/templates/project.md | 2 ++ get-shit-done/workflows/execute-phase.md | 1 + get-shit-done/workflows/new-milestone.md | 21 +++++++++++++++++++++ get-shit-done/workflows/new-project.md | 21 +++++++++++++++++++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0163b0cc0..f326c4f54 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -358,7 +358,7 @@ Equivalent paths for other runtimes: ``` .planning/ -├── PROJECT.md # Project vision, constraints, decisions +├── PROJECT.md # Project vision, constraints, decisions, evolution rules ├── REQUIREMENTS.md # Scoped requirements (v1/v2/out-of-scope) ├── ROADMAP.md # Phase breakdown with status tracking ├── STATE.md # Living memory: position, decisions, blockers, metrics diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 79e4a83d9..ad1e2402a 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -70,7 +70,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 | diff --git a/get-shit-done/templates/project.md b/get-shit-done/templates/project.md index 8971f4528..37a986c70 100644 --- a/get-shit-done/templates/project.md +++ b/get-shit-done/templates/project.md @@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform PROJECT.md evolves throughout the project lifecycle. +These rules are embedded in the generated PROJECT.md (## Evolution section) +and implemented by workflows/transition.md and workflows/complete-milestone.md. **After each phase transition:** 1. Requirements invalidated? → Move to Out of Scope with reason diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..4f778e7c2 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -214,6 +214,7 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` Read these files at execution start using the Read tool: - {phase_dir}/{plan_file} (Plan) + - .planning/PROJECT.md (Project context — core value, requirements, evolution rules) - .planning/STATE.md (State) - .planning/config.json (Config, if exists) - ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions) diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 59b894bbb..492108ef3 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -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 diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 7db0e6c6b..90597a369 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -335,6 +335,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:** From 0377951a04021e06eb55e3166466fd1d240e8076 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 07:49:14 -0400 Subject: [PATCH 32/43] feat: context window size awareness for 1M+ models (#1086) GSD was designed around 200k context windows. With Opus 4.6 / Sonnet 4.6 shipping 1M context at standard pricing, GSD should adapt its strategies. Changes: - Add context_window config option (default: 200000, configurable to 1000000) - Expose context_window in init output so workflows can adapt behavior - Update execute-phase.md to note context-size-dependent strategies: - 200k: paths-only executor context, 10-15% orchestrator budget - 1M+: richer context passing, inline small phases, relaxed /clear - Context monitor already uses percentage-based thresholds (adaptive) Users can opt in via config.json: { "context_window": 1000000 } Addresses #1086 --- get-shit-done/bin/lib/core.cjs | 2 ++ get-shit-done/bin/lib/init.cjs | 1 + get-shit-done/workflows/execute-phase.md | 13 ++++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 5038dc247..d195bfdfd 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -65,6 +65,7 @@ function loadConfig(cwd) { 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 { @@ -108,6 +109,7 @@ function loadConfig(cwd) { 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 { diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..3d92bdc6b 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -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, diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..58ddb3a46 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -191,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( @@ -652,7 +653,13 @@ Only suggest the commands listed above. Do not invent or hallucinate command nam -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 From c7954d1ad70b492c31e952254d8e91ea8e30a21f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 21:27:06 -0400 Subject: [PATCH 33/43] refactor: deduplicate code, add planningPaths helper, annotate empty catches Code quality refactoring across 9 lib modules: 1. state.cjs: Remove duplicate stateExtractField() definition (lines 12 vs 184). Replace 3 inline regex escape patterns with escapeRegex() from core.cjs. Add getStatePath() local helper for the 15 repeated statePath declarations. Import planningPaths from core. 2. core.cjs: Add planningPaths(cwd) helper returning an object with all common .planning paths (state, roadmap, project, config, phases, requirements). Add planningDir(cwd) shorthand. Export both. 3. init.cjs: Replace Unix-only execSync('find . | grep | head') with cross-platform fs.readdirSync recursive walk for code file detection. Remove unused child_process import. Fixes Windows compatibility. 4. All lib modules: Annotate 48 empty catch blocks with /* intentionally empty */ to distinguish deliberate error swallowing from accidental missing handlers. Affected: commands.cjs (8), config.cjs (1), core.cjs (4), init.cjs (13), milestone.cjs (3), phase.cjs (6), roadmap.cjs (1), state.cjs (3), verify.cjs (9). All 755 tests pass. --- get-shit-done/bin/lib/commands.cjs | 16 +++++----- get-shit-done/bin/lib/config.cjs | 2 +- get-shit-done/bin/lib/core.cjs | 31 +++++++++++++++--- get-shit-done/bin/lib/init.cjs | 49 +++++++++++++++++------------ get-shit-done/bin/lib/milestone.cjs | 6 ++-- get-shit-done/bin/lib/phase.cjs | 12 +++---- get-shit-done/bin/lib/roadmap.cjs | 2 +- get-shit-done/bin/lib/state.cjs | 34 +++++++++----------- get-shit-done/bin/lib/verify.cjs | 18 +++++------ 9 files changed, 98 insertions(+), 72 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..4dc913183 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.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) { @@ -412,7 +412,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; @@ -559,7 +559,7 @@ function cmdStats(cwd, format, raw) { status: 'Not Started', }); } - } catch {} + } catch { /* intentionally empty */ } try { const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); @@ -595,7 +595,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 +613,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 +626,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; diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 1e0e65491..ef1466f61 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -76,7 +76,7 @@ function ensureConfigFile(cwd) { delete userDefaults.depth; try { fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8'); - } catch {} + } catch { /* intentionally empty */ } } } } catch (err) { diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 5038dc247..847ea1a4f 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -76,7 +76,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) => { @@ -250,6 +250,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) { @@ -370,7 +391,7 @@ function findPhaseInternal(cwd, phase) { return result; } } - } catch {} + } catch { /* intentionally empty */ } return null; } @@ -405,7 +426,7 @@ function getArchivedPhaseDirs(cwd) { }); } } - } catch {} + } catch { /* intentionally empty */ } return results; } @@ -663,7 +684,7 @@ function getMilestonePhaseFilter(cwd) { while ((m = phasePattern.exec(roadmap)) !== null) { milestonePhaseNums.add(m[1]); } - } catch {} + } catch { /* intentionally empty */ } if (milestonePhaseNums.size === 0) { const passAll = () => true; @@ -709,4 +730,6 @@ module.exports = { replaceInCurrentMilestone, toPosixPath, MODEL_ALIAS_MAP, + planningDir, + planningPaths, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..25fab1581 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -153,7 +153,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 +167,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 +316,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 +468,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 +503,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 +552,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 +563,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 +602,7 @@ function cmdInitMapCodebase(cwd, raw) { let existingMaps = []; try { existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md')); - } catch {} + } catch { /* intentionally empty */ } const result = { // Models @@ -642,7 +651,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 +704,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 +735,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 diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 6fd032798..ba56a0727 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -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 = { diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 5cb5eac4d..dc2c52f99 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -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 @@ -818,7 +818,7 @@ 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) @@ -835,7 +835,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { break; } } - } catch {} + } catch { /* intentionally empty */ } } // Update STATE.md diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index a199a6ab8..0e6d900e7 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -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'); diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 78797694a..9296aaa5b 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -4,9 +4,14 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs'); +const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs'); +/** Shorthand — every state command needs this path */ +function getStatePath(cwd) { + return planningPaths(cwd).state; +} + // Shared helper: extract a field value from STATE.md content. // Supports both **Field:** bold and plain Field: format. function stateExtractField(content, fieldName) { @@ -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; diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 61fbc16c6..5b3a12c13 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -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,7 +689,7 @@ function cmdValidateHealth(cwd, options, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } // ─── Check 8: Run existing consistency checks ───────────────────────────── // Inline subset of cmdValidateConsistency @@ -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) { From 026a1b013e7026ed314583dafebcf42d9a3917b8 Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:29:47 +0000 Subject: [PATCH 34/43] fix(cursor): write unquoted skill and subagent names Prevent Cursor from treating frontmatter quotes as part of skill/subagent identifiers by emitting plain name scalars, and add regression tests to lock the conversion behavior. Made-with: Cursor --- bin/install.js | 15 ++++++-- tests/cursor-conversion.test.cjs | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/cursor-conversion.test.cjs diff --git a/bin/install.js b/bin/install.js index ebbaa4c68..869821dd0 100755 --- a/bin/install.js +++ b/bin/install.js @@ -710,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 }; @@ -828,7 +836,7 @@ function convertClaudeCommandToCursorSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); - return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** @@ -845,7 +853,7 @@ function convertClaudeAgentToCursorAgent(content) { const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; - const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; + const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } @@ -3257,7 +3265,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, diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs new file mode 100644 index 000000000..87652ab96 --- /dev/null +++ b/tests/cursor-conversion.test.cjs @@ -0,0 +1,61 @@ +/** + * Cursor conversion regression tests. + * + * Ensures Cursor frontmatter names are emitted as plain identifiers + * (without surrounding quotes), so Cursor does not treat quotes as + * literal parts of skill/subagent names. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +const { + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, +} = require('../bin/install.js'); + +describe('convertClaudeCommandToCursorSkill', () => { + test('writes unquoted Cursor skill name in frontmatter', () => { + const input = `--- +name: quick +description: Execute a quick task +--- + + +Test body + +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick'); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); + assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); + }); +}); + +describe('convertClaudeAgentToCursorAgent', () => { + test('writes unquoted Cursor agent name in frontmatter', () => { + const input = `--- +name: gsd-planner +description: Planner agent +tools: Read, Write +color: green +--- + + +Planner body + +`; + + const result = convertClaudeAgentToCursorAgent(input); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); + assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted'); + }); +}); From 69cfae2011ff1b3fdf31f468f9e6c891e0790f8a Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:49:28 +0000 Subject: [PATCH 35/43] fix(cursor): preserve slash-prefixed gsd commands in converted markdown Keep `/gsd-*` command examples slash-prefixed during Cursor conversion while still normalizing legacy `gsd:` syntax, and add regression coverage for Next Up markdown output. Made-with: Cursor --- bin/install.js | 10 ++++------ tests/cursor-conversion.test.cjs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/bin/install.js b/bin/install.js index fe30e5df2..ffc71fedf 100755 --- a/bin/install.js +++ b/bin/install.js @@ -766,11 +766,9 @@ function convertCursorToolName(claudeTool) { } function convertSlashCommandsToCursorSkillMentions(content) { - let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { - return `gsd-${String(commandName).toLowerCase()}`; - }); - converted = converted.replace(/\/gsd-help\b/g, 'gsd-help'); - return converted; + // 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) { @@ -1831,7 +1829,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand } 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:([a-z0-9-]+)/gi, (_, cmd) => `gsd-${cmd.toLowerCase()}`); + 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'); diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs index 87652ab96..2c7c79d52 100644 --- a/tests/cursor-conversion.test.cjs +++ b/tests/cursor-conversion.test.cjs @@ -35,6 +35,26 @@ Test body 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', () => { From a9be67f504b61bc0f91ef000839887c1083d154a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 14:54:02 -0400 Subject: [PATCH 36/43] docs: comprehensive v1.26 release documentation update (#1187) Updates all docs to reflect v1.26.0 features and changes: README.md: - Add /gsd:ship and /gsd:next to command tables - Add /gsd:session-report to Session section - Update workflow to show ship step and auto-advance - Update inherit profile description for non-Anthropic providers docs/COMMANDS.md: - Add /gsd:next command reference with full state detection logic - Add /gsd:session-report command reference with report contents docs/FEATURES.md: - Add Auto-Advance (Next) feature (#14) - Add Cross-Phase Regression Gate feature (#20) - Add Requirements Coverage Gate feature (#21) - Add Session Reporting feature (#24) - Fix all section numbering (was broken with duplicates) - Update inherit profile to mention non-Anthropic providers - Renumber all 39 features consistently docs/USER-GUIDE.md: - Add /gsd:ship to workflow diagram - Add /gsd:next and /gsd:session-report to command tables - Add HANDOFF.json and reports/ to file structure - Add troubleshooting for non-Anthropic model providers - Add recovery entries for session-report and next - Update example workflow to include ship and session-report docs/CONFIGURATION.md: - Update inherit profile to mention non-Anthropic providers --- README.md | 19 ++++- docs/COMMANDS.md | 39 ++++++++++ docs/CONFIGURATION.md | 2 +- docs/FEATURES.md | 172 +++++++++++++++++++++++++++++++----------- docs/USER-GUIDE.md | 20 ++++- 5 files changed, 201 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ddcee19a7..13e96875f 100644 --- a/README.md +++ b/README.md @@ -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 --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 ` | 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`. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 9b7947737..1e2870342 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -132,6 +132,45 @@ 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. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d1631752..bc750d3c4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -246,7 +246,7 @@ Valid override values: `opus`, `sonnet`, `haiku`, `inherit` | `quality` | Opus for all decision-making, Sonnet for verification | Quota available, critical architecture work | | `balanced` | Opus for planning only, Sonnet for everything else | Normal development (default) | | `budget` | Sonnet for code-writing, Haiku for research/verification | High-volume work, less critical phases | -| `inherit` | All agents use current session model | Dynamic model switching (OpenCode `/model`) | +| `inherit` | All agents use current session model | Dynamic model switching, **non-Anthropic providers** (OpenRouter, local models) | --- diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 79e4a83d9..5ece01d3a 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -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) --- @@ -408,9 +413,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. @@ -433,7 +463,7 @@ --- -### 15. Plan Checking +### 16. Plan Checking **Purpose:** Goal-backward verification that plans will achieve phase objectives before execution. @@ -445,7 +475,7 @@ --- -### 16. Post-Execution Verification +### 17. Post-Execution Verification **Purpose:** Automated check that the codebase delivers what the phase promised. @@ -457,7 +487,7 @@ --- -### 17. Node Repair +### 18. Node Repair **Purpose:** Autonomous recovery when task verification fails during execution. @@ -471,7 +501,7 @@ --- -### 18. Health Validation +### 19. Health Validation **Command:** `/gsd:health [--repair]` @@ -486,9 +516,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. @@ -508,7 +566,7 @@ --- -### 20. Session Management +### 23. Session Management **Commands:** `/gsd:pause-work`, `/gsd:resume-work`, `/gsd:progress` @@ -525,7 +583,32 @@ --- -### 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. @@ -539,7 +622,7 @@ --- -### 22. Model Profiles +### 26. Model Profiles **Command:** `/gsd:set-profile ` @@ -550,6 +633,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 @@ -574,7 +658,7 @@ ## Brownfield Features -### 23. Codebase Mapping +### 27. Codebase Mapping **Command:** `/gsd:map-codebase [area]` @@ -602,7 +686,7 @@ ## Utility Features -### 24. Debug System +### 28. Debug System **Command:** `/gsd:debug [description]` @@ -620,7 +704,7 @@ --- -### 25. Todo Management +### 29. Todo Management **Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos` @@ -634,7 +718,7 @@ --- -### 26. Statistics Dashboard +### 30. Statistics Dashboard **Command:** `/gsd:stats` @@ -648,7 +732,7 @@ --- -### 27. Update System +### 31. Update System **Command:** `/gsd:update` @@ -663,7 +747,7 @@ --- -### 28. Settings Management +### 32. Settings Management **Command:** `/gsd:settings` @@ -696,7 +780,7 @@ --- -### 29. Test Generation +### 33. Test Generation **Command:** `/gsd:add-tests [N]` @@ -711,7 +795,7 @@ ## Infrastructure Features -### 30. Git Integration +### 34. Git Integration **Purpose:** Atomic commits, branching strategies, and clean history management. @@ -737,7 +821,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. @@ -752,7 +836,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. @@ -775,7 +859,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. @@ -795,7 +879,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]` @@ -831,7 +915,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. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 4dcfca52b..36253a1fc 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic │ │ /gsd:verify-work │ │ <- Manual UAT │ └──────────┬─────────┘ │ │ │ │ + │ ┌──────────▼─────────┐ │ + │ │ /gsd:ship │ │ <- Create PR (optional) + │ └──────────┬─────────┘ │ + │ │ │ │ Next Phase?────────────┘ │ │ No └─────────────┼──────────────┘ @@ -285,6 +289,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. | `/gsd:execute-phase ` | Execute all plans in parallel waves | After planning is complete | | `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes | | `/gsd:ship [N]` | Create PR from verified work | After verification passes | +| `/gsd:next` | Auto-detect state and run next step | Anytime — "what should I do next?" | | `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) | | `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone | | `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified | @@ -297,6 +302,7 @@ 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 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 | @@ -426,7 +432,7 @@ Disable these to speed up phases in familiar domains or when conserving tokens. - **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`). +- **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. --- @@ -443,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 @@ -540,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. @@ -567,6 +579,8 @@ 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` | --- @@ -582,7 +596,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 From be302f02bb0d04ee013057d1f2f78067c24da380 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:07:33 -0400 Subject: [PATCH 37/43] feat: --analyze flag for discuss-phase trade-off analysis (#833) Adds --analyze flag to /gsd:discuss-phase that provides a trade-off analysis before each question (or question group in --batch mode). When active, each question is preceded by: - 2-3 options with pros/cons based on codebase context - A recommended approach with reasoning - Known pitfalls or constraints from prior phases Composable with existing flags: --batch --analyze gives grouped questions each with trade-off tables. Closes #833 --- commands/gsd/discuss-phase.md | 2 +- get-shit-done/workflows/discuss-phase.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/commands/gsd/discuss-phase.md b/commands/gsd/discuss-phase.md index b5c926021..75ebde603 100644 --- a/commands/gsd/discuss-phase.md +++ b/commands/gsd/discuss-phase.md @@ -1,7 +1,7 @@ --- name: gsd:discuss-phase description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults). -argument-hint: " [--auto]" +argument-hint: " [--auto] [--batch] [--analyze]" allowed-tools: - Read - Write diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index d8f7a6f40..89099fc13 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -406,6 +406,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 From 297adf842508ae5ebcc556d18cc81785def17a06 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:13:55 -0400 Subject: [PATCH 38/43] fix: add Windows troubleshooting for plan-phase freezes (#732) Windows users experience plan-phase freezes due to Claude Code stdio deadlocks with MCP servers. When a subagent hangs, the orchestrator blocks indefinitely with no timeout or error. Adds: - Windows troubleshooting section to plan-phase.md with: - Orphaned process cleanup (PowerShell commands) - Stale task directory cleanup (~/.claude/tasks/) - MCP server reduction advice - --skip-research fallback to reduce agent chain - Stale task directory detection to health.md (I002 diagnostic) - Reports count of stale dirs on --repair - Safe cleanup guidance Closes #732 --- get-shit-done/workflows/health.md | 22 ++++++++++++++++++++++ get-shit-done/workflows/plan-phase.md | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md index 54b7a1423..0cb95fd1b 100644 --- a/get-shit-done/workflows/health.md +++ b/get-shit-done/workflows/health.md @@ -157,3 +157,25 @@ Report final status. - Orphaned plan cleanup + + +**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze. +These are left behind when subagents are force-killed and consume disk space. + +When `--repair` is active, detect and clean up: + +```bash +# Check for stale task directories (older than 24 hours) +TASKS_DIR="$HOME/.claude/tasks" +if [ -d "$TASKS_DIR" ]; then + STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l) + if [ "$STALE_COUNT" -gt 0 ]; then + echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/" + echo " These are leftover from crashed subagent sessions." + echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)" + fi +fi +``` + +Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)` + diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 2d3291533..26697dc3e 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -737,6 +737,30 @@ Verification: {Passed | Passed with override | Skipped} ─────────────────────────────────────────────────────────────── + +**Windows users:** If plan-phase freezes during agent spawning (common on Windows due to +stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126): + +1. **Force-kill:** Close the terminal (Ctrl+C may not work) +2. **Clean up orphaned processes:** + ```powershell + # Kill orphaned node processes from stale MCP servers + Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force + ``` +3. **Clean up stale task directories:** + ```powershell + # Remove stale subagent task dirs (Claude Code never cleans these on crash) + Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue + ``` +4. **Reduce MCP server count:** Temporarily disable non-essential MCP servers in settings.json +5. **Retry:** Restart Claude Code and run `/gsd:plan-phase` again + +If freezes persist, try `--skip-research` to reduce the agent chain from 3 to 2 agents: +``` +/gsd:plan-phase N --skip-research +``` + + - [ ] .planning/ directory validated - [ ] Phase validated against roadmap From a99caaeb59cf8ffc66bda9ff3c975fa0f62311e3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:04:36 -0400 Subject: [PATCH 39/43] feat: /gsd:fast command for trivial inline tasks (#609) New lightweight command for tasks too small to justify planning overhead: typo fixes, config changes, forgotten commits, simple additions. - Runs inline in current context (no subagent spawn) - No PLAN.md or SUMMARY.md generated - Scope guard: redirects to /gsd:quick if task needs >3 file edits - Atomic commit with conventional commit format - Logs to STATE.md quick tasks table if present Complements /gsd:quick (which spawns planner+executor) for cases where the planning overhead exceeds the actual work. Closes #609 --- commands/gsd/fast.md | 30 +++++++++ get-shit-done/workflows/fast.md | 105 ++++++++++++++++++++++++++++++++ get-shit-done/workflows/help.md | 15 +++++ tests/copilot-install.test.cjs | 4 +- 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/fast.md create mode 100644 get-shit-done/workflows/fast.md diff --git a/commands/gsd/fast.md b/commands/gsd/fast.md new file mode 100644 index 000000000..4121f9f75 --- /dev/null +++ b/commands/gsd/fast.md @@ -0,0 +1,30 @@ +--- +name: gsd:fast +description: Execute a trivial task inline — no subagents, no planning overhead +argument-hint: "[task description]" +allowed-tools: + - Read + - Write + - Edit + - Bash + - Grep + - Glob +--- + + +Execute a trivial task directly in the current context without spawning subagents +or generating PLAN.md files. For tasks too small to justify planning overhead: +typo fixes, config changes, small refactors, forgotten commits, simple additions. + +This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that +needs research, multi-step planning, or verification. /gsd:fast is for tasks +you could describe in one sentence and execute in under 2 minutes. + + + +@~/.claude/get-shit-done/workflows/fast.md + + + +Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end. + diff --git a/get-shit-done/workflows/fast.md b/get-shit-done/workflows/fast.md new file mode 100644 index 000000000..729bcc32e --- /dev/null +++ b/get-shit-done/workflows/fast.md @@ -0,0 +1,105 @@ + +Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning, +no research, no plan checking. Just: understand → do → commit → log. + +For tasks like: fix a typo, update a config value, add a missing import, rename a +variable, commit uncommitted work, add a .gitignore entry, bump a version number. + +Use /gsd:quick for anything that needs multi-step planning or research. + + + + + +Parse `$ARGUMENTS` for the task description. + +If empty, ask: +``` +What's the quick fix? (one sentence) +``` + +Store as `$TASK`. + + + +**Before doing anything, verify this is actually trivial.** + +A task is trivial if it can be completed in: +- ≤ 3 file edits +- ≤ 1 minute of work +- No new dependencies or architecture changes +- No research needed + +If the task seems non-trivial (multi-file refactor, new feature, needs research), +say: + +``` +This looks like it needs planning. Use /gsd:quick instead: + /gsd:quick "{task description}" +``` + +And stop. + + + +Do the work directly: + +1. Read the relevant file(s) +2. Make the change(s) +3. Verify the change works (run existing tests if applicable, or do a quick sanity check) + +**No PLAN.md.** Just do it. + + + +Commit the change atomically: + +```bash +git add -A +git commit -m "fix: {concise description of what changed}" +``` + +Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` as appropriate. + + + +If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table. +If the table doesn't exist, skip this step silently. + +```bash +# Check if STATE.md has quick tasks table +if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then + # Append entry — workflow handles the format + echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md +fi +``` + + + +Report completion: + +``` +✅ Done: {what was changed} + Commit: {short hash} + Files: {list of changed files} +``` + +No next-step suggestions. No workflow routing. Just done. + + + + + +- NEVER spawn a Task/subagent — this runs inline +- NEVER create PLAN.md or SUMMARY.md files +- NEVER run research or plan-checking +- If the task takes more than 3 file edits, STOP and redirect to /gsd:quick +- If you're unsure how to implement it, STOP and redirect to /gsd:quick + + + +- [ ] Task completed in current context (no subagents) +- [ ] Atomic git commit with conventional message +- [ ] STATE.md updated if it exists +- [ ] Total operation under 2 minutes wall time + diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index e32ea0d25..cdd60638e 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -151,6 +151,21 @@ Usage: `/gsd:quick` Usage: `/gsd:quick --research --full` Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md` +--- + +**`/gsd:fast [description]`** +Execute a trivial task inline — no subagents, no planning files, no overhead. + +For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md. + +- No PLAN.md or SUMMARY.md created +- No subagent spawned (runs inline) +- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial +- Atomic commit with conventional message + +Usage: `/gsd:fast "fix the typo in README"` +Usage: `/gsd:fast "add .env to gitignore"` + ### Roadmap Management **`/gsd:add-phase `** diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index ea79e411d..b6510d0e7 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 42, `expected 42 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 = 42; +const EXPECTED_SKILLS = 43; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 3bfc0f68455c9043cecd7db855542b898f478c2f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:31:45 -0400 Subject: [PATCH 40/43] fix: add --no-verify support for parallel executor commits (#1116) Parallel executor agents trigger pre-commit hooks on every commit, causing build lock contention (e.g., cargo lock fights in Rust projects) and 40+ minute delays from cascading retries. Changes: - Add --no-verify flag to gsd-tools commit command (commands.cjs) - Wire --no-verify through gsd-tools.cjs CLI parser - Update execute-phase.md to instruct parallel agents to use --no-verify - Add post-wave hook validation step so orchestrator runs hooks once - Update execute-plan.md pre-commit handling: parallel agents skip hooks, sequential agents still handle hooks normally - Add parallel agent note to git-integration.md reference Fixes #1116 --- get-shit-done/bin/gsd-tools.cjs | 5 +++-- get-shit-done/bin/lib/commands.cjs | 5 +++-- get-shit-done/references/git-integration.md | 4 ++++ get-shit-done/workflows/execute-phase.md | 20 +++++++++++++++++++- get-shit-done/workflows/execute-plan.md | 8 +++++--- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index cdee42566..02915d58b 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -19,7 +19,7 @@ * state signal-resume Remove WAITING.json signal * resolve-model Get model for agent based on profile * find-phase Find phase directory by number - * commit [--files f1 f2] Commit planning docs + * commit [--files f1 f2] [--no-verify] Commit planning docs * verify-summary Verify a SUMMARY.md file * generate-slug Convert text to URL-safe slug * current-timestamp [format] Get timestamp (full|date|filename) @@ -290,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 @@ -298,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; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..b7416a1b3 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -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')) { diff --git a/get-shit-done/references/git-integration.md b/get-shit-done/references/git-integration.md index 1e0a9d1b4..d9bbecac2 100644 --- a/get-shit-done/references/git-integration.md +++ b/get-shit-done/references/git-integration.md @@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p Each task gets its own commit immediately after completion. +> **Parallel agents:** When running as a parallel executor (spawned by execute-phase), +> use `--no-verify` on all commits to avoid pre-commit hook lock contention. +> The orchestrator validates hooks once after all agents complete. + ``` {type}({phase}-{plan}): {task-name} diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..e6e855398 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -204,6 +204,14 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md. + + You are running as a PARALLEL executor agent. Use --no-verify on all git + commits to avoid pre-commit hook contention with other agents. The + orchestrator validates hooks once after all agents complete. + For gsd-tools commits: add --no-verify flag. + For direct git commits: use git commit --no-verify -m "..." + + @~/.claude/get-shit-done/workflows/execute-plan.md @~/.claude/get-shit-done/templates/summary.md @@ -240,7 +248,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 diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index e64dec825..0e5d8e8a5 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -234,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 @@ -241,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 From 60f38cdb9eaeb11ee2b0ee7bce5015b0e418c21d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:37:58 -0400 Subject: [PATCH 41/43] fix: remove duplicate stateExtractField, cross-platform code detection, STATE.md file locking Code review fixes from codebase pattern analysis: 1. state.cjs: Remove duplicate stateExtractField() definition (lines 12 vs 184). The second definition shadowed the first with identical logic. Keeps the original that uses escapeRegex() from core.cjs. 2. init.cjs: Replace Unix-only 'find' pipe with cross-platform fs.readdirSync recursive walk for code file detection. The execSync('find ... | grep ...') command fails on Windows where these Unix utilities aren't available. Removes unused child_process import. 3. state.cjs: Add lockfile-based mutual exclusion to writeStateMd() to prevent parallel executor agents from overwriting each other's STATE.md changes. Uses O_EXCL atomic file creation for lock acquisition, stale lock detection (10s timeout), and spin-wait with jitter. Ensures data integrity during wave-based parallel execution where multiple agents update STATE.md concurrently. All 755 existing tests pass. --- get-shit-done/bin/lib/init.cjs | 25 +++++++++----- get-shit-done/bin/lib/state.cjs | 59 +++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..bcac25560 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -167,17 +167,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') || diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 78797694a..ca6d1f40f 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -180,18 +180,7 @@ 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, '\\$&'); @@ -677,10 +666,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) { From 214a621cb2e6242237546be03a21a776080fa37d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:48:02 -0400 Subject: [PATCH 42/43] docs: update changelog, architecture, CLI tools, config, features, and user guide for parallel execution fixes Documentation updates for #1116 fixes and code review findings: - CHANGELOG.md: Add --no-verify commit flag, post-wave hook validation, STATE.md file locking, duplicate function removal, cross-platform init - docs/ARCHITECTURE.md: Add 'Parallel Commit Safety' section explaining --no-verify strategy and STATE.md lockfile mechanism - docs/CLI-TOOLS.md: Document --no-verify flag on commit command with usage guidance - docs/CONFIGURATION.md: Add note about pre-commit hooks and parallel execution behavior under parallelization settings - docs/FEATURES.md: Add --no-verify to executor capabilities, add 'Parallel Safety' section - docs/USER-GUIDE.md: Add troubleshooting entries for parallel execution build lock errors and Windows EPERM crashes, update recovery table --- docs/ARCHITECTURE.md | 8 ++++++++ docs/CLI-TOOLS.md | 5 ++++- docs/CONFIGURATION.md | 2 ++ docs/FEATURES.md | 5 +++++ docs/USER-GUIDE.md | 16 ++++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0163b0cc0..470df6326 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index f0228496b..2621e7769 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -331,7 +331,10 @@ node gsd-tools.cjs progress [json|table|bar] node gsd-tools.cjs todo complete # Git commit with config checks -node gsd-tools.cjs commit [--files f1 f2] [--amend] +node gsd-tools.cjs commit [--files f1 f2] [--amend] [--no-verify] +``` + +> **`--no-verify`**: Skips pre-commit hooks. Used by parallel executor agents during wave-based execution to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator runs hooks once after each wave completes. Do not use `--no-verify` during sequential execution — let hooks run normally. # Web search (requires Brave API key) node gsd-tools.cjs websearch [--limit N] [--freshness day|week|month] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index bc750d3c4..ce991803b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 5ece01d3a..b40484c3d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -245,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 diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 36253a1fc..948fa9899 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -564,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 @@ -581,6 +596,7 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat | 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` | --- From a6ba3e268e5fca4aa3112fa053451cbc88d31d5e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:36:07 -0400 Subject: [PATCH 43/43] feat: PreToolUse workflow guard hook for rogue edit prevention (#678) (#1197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New opt-in PreToolUse hook that warns when Claude edits files outside a GSD workflow context (no active /gsd: command or subagent). Soft guard — advises, does not block. The edit proceeds but Claude sees a reminder to use /gsd:fast or /gsd:quick for state tracking. Enable: set hooks.workflow_guard: true in .planning/config.json Default: disabled (false) Allows without warning: - .planning/ files (GSD state management) - Config files (.gitignore, .env, CLAUDE.md, settings.json) - Subagent contexts (executor, planner, etc.) Includes 3s stdin timeout guard and silent fail-safe. Closes #678 --- get-shit-done/workflows/settings.md | 3 +- hooks/gsd-workflow-guard.js | 93 +++++++++++++++++++++++++++++ scripts/build-hooks.js | 3 +- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 hooks/gsd-workflow-guard.js diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index b540cff61..760056918 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -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 } } ``` diff --git a/hooks/gsd-workflow-guard.js b/hooks/gsd-workflow-guard.js new file mode 100644 index 000000000..d8075aaf6 --- /dev/null +++ b/hooks/gsd-workflow-guard.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// GSD Workflow Guard — PreToolUse hook +// Detects when Claude attempts file edits outside a GSD workflow context +// (no active /gsd: command or Task subagent) and injects an advisory warning. +// +// This is a SOFT guard — it advises, not blocks. The edit still proceeds. +// The warning nudges Claude to use /gsd:quick or /gsd:fast instead of +// making direct edits that bypass state tracking. +// +// Enable via config: hooks.workflow_guard: true (default: false) +// Only triggers on Write/Edit tool calls to non-.planning/ files. + +const fs = require('fs'); +const path = require('path'); + +let input = ''; +const stdinTimeout = setTimeout(() => process.exit(0), 3000); +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + clearTimeout(stdinTimeout); + try { + const data = JSON.parse(input); + const toolName = data.tool_name; + + // Only guard Write and Edit tool calls + if (toolName !== 'Write' && toolName !== 'Edit') { + process.exit(0); + } + + // Check if we're inside a GSD workflow (Task subagent or /gsd: command) + // Subagents have a session_id that differs from the parent + // and typically have a description field set by the orchestrator + if (data.tool_input?.is_subagent || data.session_type === 'task') { + process.exit(0); + } + + // Check the file being edited + const filePath = data.tool_input?.file_path || data.tool_input?.path || ''; + + // Allow edits to .planning/ files (GSD state management) + if (filePath.includes('.planning/') || filePath.includes('.planning\\')) { + process.exit(0); + } + + // Allow edits to common config/docs files that don't need GSD tracking + const allowedPatterns = [ + /\.gitignore$/, + /\.env/, + /CLAUDE\.md$/, + /AGENTS\.md$/, + /GEMINI\.md$/, + /settings\.json$/, + ]; + if (allowedPatterns.some(p => p.test(filePath))) { + process.exit(0); + } + + // Check if workflow guard is enabled + const cwd = data.cwd || process.cwd(); + const configPath = path.join(cwd, '.planning', 'config.json'); + if (fs.existsSync(configPath)) { + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (!config.hooks?.workflow_guard) { + process.exit(0); // Guard disabled (default) + } + } catch (e) { + process.exit(0); + } + } else { + process.exit(0); // No GSD project — don't guard + } + + // If we get here: GSD project, guard enabled, file edit outside .planning/, + // not in a subagent context. Inject advisory warning. + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` + + 'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' + + 'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' + + 'to maintain project state tracking. ' + + 'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.' + } + }; + + process.stdout.write(JSON.stringify(output)); + } catch (e) { + // Silent fail — never block tool execution + process.exit(0); + } +}); diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index dda263e4e..b1b8fa416 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -17,7 +17,8 @@ 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' ]; /**