fix(#2418,#2399,#2419,#2421): four workflow and installer bug fixes (#2462)

- #2418: convertClaudeToAntigravityContent now replaces bare ~/.claude and
  $HOME/.claude (no trailing slash) for both global and local installs,
  eliminating the "unreplaced .claude path reference" warnings in
  gsd-debugger.md and update.md during Antigravity installs.

- #2399: plan-phase workflow gains step 13c that commits PLAN.md files
  and STATE.md via gsd-sdk query commit when commit_docs is true.
  Previously commit_docs:true was read but never acted on in plan-phase.

- #2419: new-project.md and new-milestone.md now parse agents_installed
  and missing_agents from the init JSON and warn users clearly when GSD
  agents are not installed, rather than silently failing with "agent type
  not found" when trying to spawn gsd-project-researcher subagents.

- #2421: gsd-planner.md gains a "Grep gate hygiene" rule immediately after
  the Nyquist Rule explaining the self-invalidating grep gate anti-pattern
  and providing comment-stripping alternatives (grep -v, ast-grep).

Tests: 4 new test files (30 tests) all passing.

Closes #2418
Closes #2399
Closes #2419
Closes #2421

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-20 10:09:33 -04:00
committed by GitHub
parent 4cd890b252
commit dfa1ecce99
9 changed files with 431 additions and 2 deletions

View File

@@ -215,6 +215,8 @@ Every task has four required fields:
**Nyquist Rule:** Every `<verify>` must include an `<automated>` command. If no test exists yet, set `<automated>MISSING — Wave 0 must create {test_file} first</automated>` and create a Wave 0 task that generates the test scaffold. **Nyquist Rule:** Every `<verify>` must include an `<automated>` command. If no test exists yet, set `<automated>MISSING — Wave 0 must create {test_file} first</automated>` 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.
**<done>:** Acceptance criteria - measurable state of completion. **<done>:** Acceptance criteria - measurable state of completion.
- Good: "Valid credentials return 200 + JWT cookie, invalid credentials return 401" - Good: "Valid credentials return 200 + JWT cookie, invalid credentials return 401"
- Bad: "Authentication is complete" - Bad: "Authentication is complete"

View File

@@ -1006,9 +1006,15 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) {
if (isGlobal) { if (isGlobal) {
c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/'); c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
c = c.replace(/~\/\.claude\//g, '~/.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 { } else {
c = c.replace(/\$HOME\/\.claude\//g, '.agent/'); c = c.replace(/\$HOME\/\.claude\//g, '.agent/');
c = c.replace(/~\/\.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/');
c = c.replace(/\.claude\//g, '.agent/'); c = c.replace(/\.claude\//g, '.agent/');

View File

@@ -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) 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`) ## 7.5 Reset-phase safety (only when `--reset-phase-numbers`)

View File

@@ -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) 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 67 (parallel research and synthesis) and proceed directly to roadmap creation in Step 8.
**Detect runtime and set instruction file name:** **Detect runtime and set instruction file name:**

View File

@@ -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. 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 ## 14. Present Final Status
Route to `<offer_next>` OR `auto_advance` depending on flags/config. Route to `<offer_next>` OR `auto_advance` depending on flags/config.

View File

@@ -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)'
);
});
});

View File

@@ -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}`
);
});
});
});

View File

@@ -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(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
assert.ok(agentTypesMatch, 'new-project.md must have <available_agent_types> section');
assert.ok(
agentTypesMatch[1].includes('gsd-project-researcher'),
'new-project.md <available_agent_types> 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(/<available_agent_types>([\s\S]*?)<\/available_agent_types>/);
assert.ok(agentTypesMatch, 'new-milestone.md must have <available_agent_types> section');
assert.ok(
agentTypesMatch[1].includes('gsd-project-researcher'),
'new-milestone.md <available_agent_types> must list gsd-project-researcher'
);
});
});

View File

@@ -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
* <automated> 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})`
);
});
});