From 31a93e2da726964f0b09994b285034baaa58f1aa Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:07 -0400 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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 12/27] 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 13/27] 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 14/27] 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 15/27] 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 16/27] 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 17/27] =?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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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) {