mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
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:
@@ -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"
|
||||||
|
|||||||
@@ -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/');
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|
||||||
|
|||||||
@@ -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 6–7 (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:**
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
72
tests/bug-2399-commit-docs-plan-phase.test.cjs
Normal file
72
tests/bug-2399-commit-docs-plan-phase.test.cjs
Normal 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)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
146
tests/bug-2418-antigravity-bare-path.test.cjs
Normal file
146
tests/bug-2418-antigravity-bare-path.test.cjs
Normal 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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
96
tests/bug-2419-project-researcher-agent.test.cjs
Normal file
96
tests/bug-2419-project-researcher-agent.test.cjs
Normal 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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
tests/bug-2421-planner-grep-gate-hygiene.test.cjs
Normal file
69
tests/bug-2421-planner-grep-gate-hygiene.test.cjs
Normal 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})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user