diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index d5101a3d..3fb603cb 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -215,6 +215,8 @@ Every task has four required fields: **Nyquist Rule:** Every `` must include an `` command. If no test exists yet, set `MISSING — Wave 0 must create {test_file} first` and create a Wave 0 task that generates the test scaffold. +**Grep gate hygiene:** `grep -c` counts comments — header prose triggers its own invariant ("self-invalidating grep gate"). Use `grep -v '^#' | grep -c token`. Bare `== 0` gates on unfiltered files are forbidden. + **:** Acceptance criteria - measurable state of completion. - Good: "Valid credentials return 200 + JWT cookie, invalid credentials return 401" - Bad: "Authentication is complete" diff --git a/bin/install.js b/bin/install.js index 30bf7742..2ae0466f 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1006,9 +1006,15 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) { if (isGlobal) { c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/'); c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/'); + // Bare form (no trailing slash) — must come after slash form to avoid double-replace + c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity'); + c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity'); } else { c = c.replace(/\$HOME\/\.claude\//g, '.agent/'); c = c.replace(/~\/\.claude\//g, '.agent/'); + // Bare form (no trailing slash) — must come after slash form to avoid double-replace + c = c.replace(/\$HOME\/\.claude\b/g, '.agent'); + c = c.replace(/~\/\.claude\b/g, '.agent'); } c = c.replace(/\.\/\.claude\//g, './.agent/'); c = c.replace(/\.claude\//g, '.agent/'); diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index b7b0afa1..4ec4b9f5 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -208,7 +208,21 @@ AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-synthesizer 2>/dev/nul AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper 2>/dev/null) ``` -Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`. +Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`, `agents_installed`, `missing_agents`. + +**If `agents_installed` is false:** Display a warning before proceeding: +``` +⚠ GSD agents not installed. The following agents are missing from your agents directory: + {missing_agents joined with newline} + +Subagent spawns (gsd-project-researcher, gsd-research-synthesizer, gsd-roadmapper) will fail +with "agent type not found". Run the installer with --global to make agents available: + + npx get-shit-done-cc@latest --global + +Proceeding without research subagents — roadmap will be generated inline. +``` +Skip the parallel research spawn step and generate the roadmap inline. ## 7.5 Reset-phase safety (only when `--reset-phase-numbers`) diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index b7aef797..8c0f662f 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -64,7 +64,21 @@ AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-synthesizer 2>/dev/nul AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper 2>/dev/null) ``` -Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`. +Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`, `agents_installed`, `missing_agents`. + +**If `agents_installed` is false:** Display a warning before proceeding: +``` +⚠ GSD agents not installed. The following agents are missing from your agents directory: + {missing_agents joined with newline} + +Subagent spawns (gsd-project-researcher, gsd-research-synthesizer, gsd-roadmapper) will fail +with "agent type not found". Run the installer with --global to make agents available: + + npx get-shit-done-cc@latest --global + +Proceeding without research subagents — roadmap will be generated inline. +``` +Skip Steps 6–7 (parallel research and synthesis) and proceed directly to roadmap creation in Step 8. **Detect runtime and set instruction file name:** diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 741a5ea2..8346f961 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -1145,6 +1145,16 @@ gsd-sdk query state.planned-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME This updates STATUS to "Ready to execute", sets the correct plan count, and timestamps Last Activity. +## 13c. Commit Plans if commit_docs is true + +If `commit_docs` is true (from the init JSON parsed in step 1), commit the generated plan artifacts: + +```bash +gsd-sdk query commit "docs(${PADDED_PHASE}): create phase plan" --files "${PHASE_DIR}"/*-PLAN.md .planning/STATE.md +``` + +This commits all PLAN.md files for the phase plus the updated STATE.md to version-control the planning artifacts. Skip this step if `commit_docs` is false. + ## 14. Present Final Status Route to `` OR `auto_advance` depending on flags/config. diff --git a/tests/bug-2399-commit-docs-plan-phase.test.cjs b/tests/bug-2399-commit-docs-plan-phase.test.cjs new file mode 100644 index 00000000..2ef121fc --- /dev/null +++ b/tests/bug-2399-commit-docs-plan-phase.test.cjs @@ -0,0 +1,72 @@ +/** + * Bug #2399: commit_docs:true is ignored in plan-phase + * + * The plan-phase workflow generates plan artifacts but never commits them even + * when commit_docs is true. A step between 13b and 14 must commit the PLAN.md + * files and updated STATE.md when commit_docs is set. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const PLAN_PHASE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'plan-phase.md'); + +describe('plan-phase commit_docs support (#2399)', () => { + test('plan-phase.md exists', () => { + assert.ok(fs.existsSync(PLAN_PHASE_PATH), 'get-shit-done/workflows/plan-phase.md must exist'); + }); + + test('plan-phase.md has a commit step for plan artifacts', () => { + const content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8'); + // Must contain a commit call that references PLAN.md files + assert.ok( + content.includes('PLAN.md') && content.includes('commit'), + 'plan-phase.md must include a commit step that references PLAN.md files' + ); + }); + + test('plan-phase.md commit step is gated on commit_docs', () => { + const content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8'); + // The commit step must be conditional on commit_docs + assert.ok( + content.includes('commit_docs'), + 'plan-phase.md must reference commit_docs to gate the plan commit step' + ); + }); + + test('plan-phase.md commit step references STATE.md', () => { + const content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8'); + // Should commit STATE.md alongside PLAN.md files + assert.ok( + content.includes('STATE.md'), + 'plan-phase.md commit step should include STATE.md to capture planning completion state' + ); + }); + + test('plan-phase.md has a step 13c that commits plan artifacts', () => { + const content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8'); + const step13b = content.indexOf('## 13b.'); + const step14 = content.indexOf('## 14.'); + // Look for the step 13c section (or any commit step between 13b and 14) + const step13c = content.indexOf('## 13c.'); + + assert.ok(step13b !== -1, '## 13b. section must exist'); + assert.ok(step14 !== -1, '## 14. section must exist'); + assert.ok(step13c !== -1, '## 13c. step must exist (commit plans step)'); + assert.ok( + step13c > step13b && step13c < step14, + `Step 13c (at ${step13c}) must appear between step 13b (at ${step13b}) and step 14 (at ${step14})` + ); + }); + + test('plan-phase.md uses gsd-sdk query commit for the plan commit', () => { + const content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8'); + // Must use gsd-sdk query commit (not raw git) so commit_docs guard in gsd-tools is respected + assert.ok( + content.includes('gsd-sdk query commit') || content.includes('gsd-tools') || content.includes('gsd-sdk'), + 'plan-phase.md plan commit step must use gsd-sdk query commit (not raw git commit)' + ); + }); +}); diff --git a/tests/bug-2418-antigravity-bare-path.test.cjs b/tests/bug-2418-antigravity-bare-path.test.cjs new file mode 100644 index 00000000..769ed279 --- /dev/null +++ b/tests/bug-2418-antigravity-bare-path.test.cjs @@ -0,0 +1,146 @@ +/** + * Bug #2418: Found unreplaced .claude path reference(s) in Antigravity install + * + * The Antigravity path converter handles ~/.claude/ (with trailing slash) but + * misses bare ~/.claude (without trailing slash), leaving unreplaced references + * that cause the installer to warn about leaked paths. + * + * Files affected: agents/gsd-debugger.md (configDir = ~/.claude) and + * get-shit-done/workflows/update.md (comment with e.g. ~/.claude). + */ + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { convertClaudeToAntigravityContent } = require('../bin/install.js'); + +describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () => { + describe('global install', () => { + test('replaces ~/.claude (bare, no trailing slash) with ~/.gemini/antigravity', () => { + const input = 'configDir = ~/.claude'; + const result = convertClaudeToAntigravityContent(input, true); + assert.ok( + result.includes('~/.gemini/antigravity'), + `Expected ~/.gemini/antigravity in output, got: ${result}` + ); + assert.ok( + !result.includes('~/.claude'), + `Expected ~/ .claude to be replaced, got: ${result}` + ); + }); + + test('replaces $HOME/.claude (bare, no trailing slash) with $HOME/.gemini/antigravity', () => { + const input = 'export DIR=$HOME/.claude'; + const result = convertClaudeToAntigravityContent(input, true); + assert.ok( + result.includes('$HOME/.gemini/antigravity'), + `Expected $HOME/.gemini/antigravity in output, got: ${result}` + ); + assert.ok( + !result.includes('$HOME/.claude'), + `Expected $HOME/.claude to be replaced, got: ${result}` + ); + }); + + test('handles bare ~/.claude followed by comma (comment context)', () => { + const input = '# e.g. ~/.claude, ~/.config/opencode'; + const result = convertClaudeToAntigravityContent(input, true); + assert.ok( + !result.includes('~/.claude'), + `Expected ~/ .claude to be replaced in comment context, got: ${result}` + ); + }); + + test('still replaces ~/.claude/ (with trailing slash) correctly', () => { + const input = 'See ~/.claude/get-shit-done/workflows/'; + const result = convertClaudeToAntigravityContent(input, true); + assert.ok( + result.includes('~/.gemini/antigravity/get-shit-done/workflows/'), + `Expected path with trailing slash to be replaced, got: ${result}` + ); + assert.ok(!result.includes('~/.claude/'), `Expected ~/ .claude/ to be fully replaced, got: ${result}`); + }); + + test('does not double-replace ~/.claude/ paths', () => { + const input = 'See ~/.claude/get-shit-done/'; + const result = convertClaudeToAntigravityContent(input, true); + // Result should contain exactly one occurrence of the replacement path + const count = (result.match(/~\/.gemini\/antigravity\//g) || []).length; + assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`); + }); + }); + + describe('local install', () => { + test('replaces ~/.claude (bare, no trailing slash) with .agent', () => { + const input = 'configDir = ~/.claude'; + const result = convertClaudeToAntigravityContent(input, false); + assert.ok( + result.includes('.agent'), + `Expected .agent in output, got: ${result}` + ); + assert.ok( + !result.includes('~/.claude'), + `Expected ~/ .claude to be replaced, got: ${result}` + ); + }); + + test('replaces $HOME/.claude (bare, no trailing slash) with .agent', () => { + const input = 'export DIR=$HOME/.claude'; + const result = convertClaudeToAntigravityContent(input, false); + assert.ok( + result.includes('.agent'), + `Expected .agent in output, got: ${result}` + ); + assert.ok( + !result.includes('$HOME/.claude'), + `Expected $HOME/.claude to be replaced, got: ${result}` + ); + }); + + test('does not double-replace ~/.claude/ paths', () => { + const input = 'See ~/.claude/get-shit-done/'; + const result = convertClaudeToAntigravityContent(input, false); + // .agent/ should appear exactly once + const count = (result.match(/\.agent\//g) || []).length; + assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`); + }); + }); + + describe('installed files contain no bare ~/.claude references after conversion', () => { + const fs = require('fs'); + const path = require('path'); + const repoRoot = path.join(__dirname, '..'); + + // The scanner regex used by the installer to detect leaked paths + const leakedPathRegex = /(?:~|\$HOME)\/\.claude\b/g; + + function convertFile(filePath, isGlobal) { + const content = fs.readFileSync(filePath, 'utf8'); + return convertClaudeToAntigravityContent(content, isGlobal); + } + + test('gsd-debugger.md has no leaked ~/.claude after global Antigravity conversion', () => { + const debuggerPath = path.join(repoRoot, 'agents', 'gsd-debugger.md'); + if (!fs.existsSync(debuggerPath)) return; // skip if file doesn't exist + const converted = convertFile(debuggerPath, true); + const matches = converted.match(leakedPathRegex); + assert.strictEqual( + matches, null, + `gsd-debugger.md still contains leaked .claude paths after Antigravity conversion: ${matches}` + ); + }); + + test('update.md has no leaked ~/.claude after global Antigravity conversion', () => { + const updatePath = path.join(repoRoot, 'get-shit-done', 'workflows', 'update.md'); + if (!fs.existsSync(updatePath)) return; // skip if file doesn't exist + const converted = convertFile(updatePath, true); + const matches = converted.match(leakedPathRegex); + assert.strictEqual( + matches, null, + `update.md still contains leaked .claude paths after Antigravity conversion: ${matches}` + ); + }); + }); +}); diff --git a/tests/bug-2419-project-researcher-agent.test.cjs b/tests/bug-2419-project-researcher-agent.test.cjs new file mode 100644 index 00000000..0a0c0249 --- /dev/null +++ b/tests/bug-2419-project-researcher-agent.test.cjs @@ -0,0 +1,96 @@ +/** + * Bug #2419: gsd-project-researcher agent type not found + * + * When gsd-new-project spawns gsd-project-researcher subagents, it fails with + * "agent type not found" if the user has a local-only install (agents in + * .claude/agents/ of a different project, not the global ~/.claude/agents/). + * + * Fix: new-project.md and new-milestone.md must parse agents_installed from + * the init JSON and warn the user (rather than silently failing) when agents + * are missing. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const NEW_PROJECT_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'new-project.md'); +const NEW_MILESTONE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'new-milestone.md'); +const AGENTS_DIR = path.join(__dirname, '..', 'agents'); + +describe('gsd-project-researcher agent registration (#2419)', () => { + test('gsd-project-researcher.md exists in agents source dir', () => { + const agentFile = path.join(AGENTS_DIR, 'gsd-project-researcher.md'); + assert.ok( + fs.existsSync(agentFile), + 'agents/gsd-project-researcher.md must exist in the source agents directory' + ); + }); + + test('gsd-project-researcher.md has correct name in frontmatter', () => { + const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-project-researcher.md'), 'utf-8'); + assert.ok( + content.includes('name: gsd-project-researcher'), + 'agents/gsd-project-researcher.md must have name: gsd-project-researcher in frontmatter' + ); + }); + + test('new-project.md parses agents_installed from init JSON', () => { + const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8'); + assert.ok( + content.includes('agents_installed'), + 'new-project.md must parse agents_installed from the init JSON to detect missing agents' + ); + }); + + test('new-project.md warns user when agents_installed is false', () => { + const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8'); + assert.ok( + content.includes('agents_installed') && content.includes('agent type not found') || + content.includes('agents_installed') && content.includes('missing') || + content.includes('agents_installed') && content.includes('not installed'), + 'new-project.md must warn the user when agents are not installed (agents_installed is false)' + ); + }); + + test('new-milestone.md parses agents_installed from init JSON', () => { + const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8'); + assert.ok( + content.includes('agents_installed'), + 'new-milestone.md must parse agents_installed from the init JSON to detect missing agents' + ); + }); + + test('new-milestone.md warns user when agents_installed is false', () => { + const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8'); + assert.ok( + content.includes('agents_installed') && ( + content.includes('agent type not found') || + content.includes('missing') || + content.includes('not installed') + ), + 'new-milestone.md must warn the user when agents are not installed (agents_installed is false)' + ); + }); + + test('new-project.md lists gsd-project-researcher in available_agent_types', () => { + const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8'); + const agentTypesMatch = content.match(/([\s\S]*?)<\/available_agent_types>/); + assert.ok(agentTypesMatch, 'new-project.md must have section'); + assert.ok( + agentTypesMatch[1].includes('gsd-project-researcher'), + 'new-project.md must list gsd-project-researcher' + ); + }); + + test('new-milestone.md lists gsd-project-researcher in available_agent_types', () => { + const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8'); + const agentTypesMatch = content.match(/([\s\S]*?)<\/available_agent_types>/); + assert.ok(agentTypesMatch, 'new-milestone.md must have section'); + assert.ok( + agentTypesMatch[1].includes('gsd-project-researcher'), + 'new-milestone.md must list gsd-project-researcher' + ); + }); +}); diff --git a/tests/bug-2421-planner-grep-gate-hygiene.test.cjs b/tests/bug-2421-planner-grep-gate-hygiene.test.cjs new file mode 100644 index 00000000..3de0f63e --- /dev/null +++ b/tests/bug-2421-planner-grep-gate-hygiene.test.cjs @@ -0,0 +1,69 @@ +/** + * Bug #2421: gsd-planner emits grep-count acceptance gates that count comment text + * + * The planner must instruct agents to use comment-aware grep patterns in + * verify blocks. Without this, descriptive comments in file + * headers count against the gate and force authors to reword them — the + * "self-invalidating grep gate" anti-pattern. + */ + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md'); + +describe('gsd-planner grep gate hygiene (#2421)', () => { + test('gsd-planner.md exists in agents source dir', () => { + assert.ok(fs.existsSync(PLANNER_PATH), 'agents/gsd-planner.md must exist'); + }); + + test('gsd-planner.md contains Grep gate hygiene rule', () => { + const content = fs.readFileSync(PLANNER_PATH, 'utf-8'); + assert.ok( + content.includes('Grep gate hygiene') || content.includes('grep gate hygiene'), + 'gsd-planner.md must contain a "Grep gate hygiene" rule to prevent self-invalidating grep gates' + ); + }); + + test('gsd-planner.md explains self-invalidating grep gate anti-pattern', () => { + const content = fs.readFileSync(PLANNER_PATH, 'utf-8'); + assert.ok( + content.includes('self-invalidating'), + 'gsd-planner.md must describe the "self-invalidating" grep gate anti-pattern' + ); + }); + + test('gsd-planner.md provides comment-stripping grep example', () => { + const content = fs.readFileSync(PLANNER_PATH, 'utf-8'); + // Must show a pattern that excludes comment lines (grep -v or grep -vE) + assert.ok( + content.includes('grep -v') || content.includes('grep -vE') || content.includes('-v '), + 'gsd-planner.md must provide a comment-stripping grep example (grep -v or grep -vE)' + ); + }); + + test('gsd-planner.md warns against bare zero-count grep gates on whole files', () => { + const content = fs.readFileSync(PLANNER_PATH, 'utf-8'); + assert.ok( + content.includes('== 0') || content.includes('zero-count') || content.includes('zero count'), + 'gsd-planner.md must warn against bare zero-count grep gates without comment exclusion' + ); + }); + + test('gsd-planner.md grep gate hygiene rule appears after Nyquist Rule', () => { + const content = fs.readFileSync(PLANNER_PATH, 'utf-8'); + const nyquistIdx = content.indexOf('Nyquist Rule'); + const grepGateIdx = content.indexOf('grep gate hygiene') !== -1 + ? content.indexOf('grep gate hygiene') + : content.indexOf('Grep gate hygiene'); + + assert.ok(nyquistIdx !== -1, 'Nyquist Rule must be present in gsd-planner.md'); + assert.ok(grepGateIdx !== -1, 'Grep gate hygiene must be present in gsd-planner.md'); + assert.ok( + grepGateIdx > nyquistIdx, + `Grep gate hygiene rule (at ${grepGateIdx}) must appear after Nyquist Rule (at ${nyquistIdx})` + ); + }); +});