test(#1976): consolidate 17 agent regression tests into agent-contract suites

Fold 17 issue-named agent-markdown regression files into their canonical agent
suites: agent-frontmatter (shared agent-body-contract catch-all: read-loop guards,
retired-slash-ref bans, sycophancy hardening, doc-writer/code-fixer/review-fix/mapper
contracts, write-truncation), planner-language-regression (planner directive/grep/phase
contract), executor-mvp-tdd-section, verifier-behavior-unverified, intel, and
research-agent-profiles. Verbatim block-scoped describe wrappers; 98 subtests conserved
1:1. All unit (markdown-content) tests — no CLI/env surface. No new files.

Regenerates regression-name allowlist (222->207), ratchets file-count allowlist (intel
entry removed, phase 4->3), makes 13 relocated allow-test-rule exemptions issue-ref-
compliant (ADR-456; prunes stale ids). No CONTEXT.md/ADR refs named the moved files.
lint:ci green.

Part of epic #1969. Closes #1976.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-03 03:08:11 -04:00
parent 08f9956019
commit cb7ca4f3dc
26 changed files with 1964 additions and 1842 deletions

View File

@@ -17,28 +17,17 @@
"tests/bug-131-release-tarball-smoke-explicit-home.test.cjs :: integration-test-input",
"tests/bug-211-launcher-home-fallback.test.cjs :: structural/behavioral regression for the ~/.claude fallback arm in",
"tests/bug-2136-sh-hook-version.test.cjs :: structural-regression-guard",
"tests/bug-214-phase-researcher-write-truncation-contract.test.cjs :: source-text-is-the-product",
"tests/bug-214-writer-agents-write-truncation-contract.test.cjs :: source-text-is-the-product",
"tests/bug-2346-agent-read-loop-guards.test.cjs :: source-text-is-the-product",
"tests/bug-2419-project-researcher-agent.test.cjs :: source-text-is-the-product",
"tests/bug-2421-planner-grep-gate-hygiene.test.cjs :: source-text-is-the-product",
"tests/bug-2543-gsd-slash-namespace.test.cjs :: structural-regression-guard",
"tests/bug-2559-stale-search-year.test.cjs :: source-text-is-the-product",
"tests/bug-2686-review-fix-worktree.test.cjs :: source-text-is-the-product",
"tests/bug-2772-gitmodules-path-intersection.test.cjs :: source-text-is-the-product",
"tests/bug-2808-skill-hyphen-name.test.cjs :: source-text-is-the-product",
"tests/bug-2839-review-fix-transactional-cleanup.test.cjs :: source-text-is-the-product",
"tests/bug-2990-code-fixer-worktree-branch.test.cjs :: source-text-is-the-product",
"tests/bug-3097-3099-executor-worktree-path-safety.test.cjs :: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep",
"tests/bug-3290-intel-updater-layout-block.test.cjs :: source-text-is-the-product — agents/gsd-intel-updater.md IS",
"tests/bug-33-settings-model-profile-adaptive.test.cjs :: source-text-is-the-product",
"tests/bug-3384-secondary-defects.test.cjs :: source-text-is-the-product",
"tests/bug-3430-planner-phase-contract.test.cjs :: source-text-is-the-product",
"tests/bug-3446-resume-continue-here-discovery.test.cjs :: source-text-is-the-product",
"tests/bug-3491-nested-git-worktree.test.cjs :: source-text-is-the-product",
"tests/bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs :: validates runtime CLI stdout/stderr warning behavior, not source grep",
"tests/bug-3542-executor-git-stash-prohibition.test.cjs :: source-text-is-the-product",
"tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs :: source-text-is-the-product",
"tests/bug-3677-agent-colon-namespace-leak.test.cjs :: source-text-is-the-product",
"tests/bug-3678-executor-commit-docs-respect.test.cjs :: source-text-is-the-product",
"tests/bug-3683-command-colon-namespace-leak.test.cjs :: source-text-is-the-product",
@@ -46,7 +35,6 @@
"tests/bug-3689-resume-glob-nomatch.test.cjs :: source-text-is-the-product",
"tests/bug-3810-no-gsd-sdk-runtime-refs.test.cjs :: source-text-is-the-product",
"tests/bug-444-resolver-local-claude-install.test.cjs :: structural/behavioral regression for the repo-local .claude/ install",
"tests/bug-571-doc-writer-fix-mode-edit-only.test.cjs :: source-text-is-the-product",
"tests/bug-619-codebase-drift-gate-shim.test.cjs :: source-text-is-the-product",
"tests/bug-622-graphify-optional-graph-html.test.cjs :: source-text-is-the-product",
"tests/bug-630-wave-cleanup-orchestrator-root.test.cjs :: source-text-is-the-product",
@@ -91,7 +79,6 @@
"tests/edge-probe-spec-phase-contract.test.cjs :: runtime-contract-is-the-product — spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion",
"tests/edit-phase.test.cjs :: source-text-is-the-product",
"tests/enh-2380-sync-skills.test.cjs :: source-text-is-the-product",
"tests/enh-2500-codebase-mapper-arch-rich-format.test.cjs :: source-text-is-the-product",
"tests/enh-2789-description-budget.test.cjs :: source-text-is-the-product",
"tests/enh-2790-skill-consolidation.test.cjs :: source-text-is-the-product",
"tests/enh-48-cwd-drift-guard-e2e.test.cjs :: integration-test-input",

View File

@@ -6,37 +6,24 @@
"bug-21-state-md-template-frontmatter.test.cjs",
"bug-211-launcher-home-fallback.test.cjs",
"bug-2136-sh-hook-version.test.cjs",
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
"bug-214-writer-agents-write-truncation-contract.test.cjs",
"bug-2344-read-guard-claudecode-env.test.cjs",
"bug-2346-agent-read-loop-guards.test.cjs",
"bug-2351-intel-kilo-layout.test.cjs",
"bug-2419-project-researcher-agent.test.cjs",
"bug-2421-planner-grep-gate-hygiene.test.cjs",
"bug-2451-context-monitor-over-report.test.cjs",
"bug-2520-read-guard-hook-subprocess-env.test.cjs",
"bug-2543-gsd-slash-namespace.test.cjs",
"bug-2559-stale-search-year.test.cjs",
"bug-260-worktree-path-guard.test.cjs",
"bug-261-worktree-force-add-guard.test.cjs",
"bug-2686-review-fix-worktree.test.cjs",
"bug-2772-gitmodules-path-intersection.test.cjs",
"bug-2808-skill-hyphen-name.test.cjs",
"bug-2839-review-fix-transactional-cleanup.test.cjs",
"bug-2866-codex-strip-no-trailing-newline.test.cjs",
"bug-2876-skill-frontmatter-quote.test.cjs",
"bug-2916-handle-branching-default-base.test.cjs",
"bug-2990-code-fixer-worktree-branch.test.cjs",
"bug-2995-post-install-script-paths.test.cjs",
"bug-3019-help-passthrough.test.cjs",
"bug-3054-stale-gsd-next-references.test.cjs",
"bug-3087-planner-directive-language.test.cjs",
"bug-3097-3099-executor-worktree-path-safety.test.cjs",
"bug-3290-intel-updater-layout-block.test.cjs",
"bug-33-settings-model-profile-adaptive.test.cjs",
"bug-3321-verifier-runs-probes.test.cjs",
"bug-3384-secondary-defects.test.cjs",
"bug-3430-planner-phase-contract.test.cjs",
"bug-3442-shim-projection-drift-guard.test.cjs",
"bug-3446-resume-continue-here-discovery.test.cjs",
"bug-3491-nested-git-worktree.test.cjs",
@@ -44,7 +31,6 @@
"bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs",
"bug-3542-executor-git-stash-prohibition.test.cjs",
"bug-3588-npm-audit-clean.test.cjs",
"bug-3605-stale-research-insert-phase-agent-refs.test.cjs",
"bug-3668-workflow-runtime-resolution.test.cjs",
"bug-3677-agent-colon-namespace-leak.test.cjs",
"bug-3678-executor-commit-docs-respect.test.cjs",
@@ -53,7 +39,6 @@
"bug-3689-resume-glob-nomatch.test.cjs",
"bug-3810-no-gsd-sdk-runtime-refs.test.cjs",
"bug-444-resolver-local-claude-install.test.cjs",
"bug-571-doc-writer-fix-mode-edit-only.test.cjs",
"bug-619-codebase-drift-gate-shim.test.cjs",
"bug-622-graphify-optional-graph-html.test.cjs",
"bug-630-wave-cleanup-orchestrator-root.test.cjs",

View File

@@ -21,15 +21,6 @@
],
"issue": "622"
},
"intel": {
"files": [
"bug-2351-intel-kilo-layout.test.cjs",
"bug-3290-intel-updater-layout-block.test.cjs",
"intel-command-cutover.test.cjs",
"intel.test.cjs"
],
"issue": "TBD"
},
"milestone": {
"files": [
"milestone-archive.test.cjs",
@@ -40,14 +31,6 @@
],
"issue": "TBD"
},
"phase": {
"files": [
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
"phase-dependency-levels.test.cjs",
"phase.test.cjs"
],
"issue": "597"
},
"roadmap": {
"files": [
"roadmap-mode-field.test.cjs",

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +0,0 @@
// allow-test-rule: source-text-is-the-product
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.resolve(__dirname, '..');
const RESEARCHER_PATH = path.join(REPO_ROOT, 'agents', 'gsd-phase-researcher.md');
function readResearcherPrompt() {
return fs.readFileSync(RESEARCHER_PATH, 'utf8');
}
describe('bug #214: phase researcher must survive OpenCode write-tool truncation', () => {
test('Step 6 documents the large-file / truncation fallback write contract', () => {
const prompt = readResearcherPrompt();
assert.match(
prompt,
/truncat/i,
'Step 6 must name the truncation failure mode.'
);
assert.match(
prompt,
/incrementa/i,
'Step 6 must instruct incremental construction on large files.'
);
assert.match(
prompt,
/<!-- gsd:write-continue -->/,
'Step 6 must define the continuation sentinel for incremental writes.'
);
assert.match(
prompt,
/do NOT retry the same oversized call/i,
'Step 6 must forbid identical retry of the oversized write (doom-loop guard).'
);
assert.match(
prompt,
/do NOT silently fall back to returning content/i,
'Step 6 must forbid silent fallback to returning content.'
);
assert.match(
prompt,
/`Read` the file, then `Edit`/i,
'Step 6 must require Read before Edit (OpenCode edit requires a prior Read).'
);
assert.match(
prompt,
/no trailing sentinel/i,
'Step 6 must instruct removing the sentinel on the final section.'
);
assert.match(
prompt,
/write the whole file in a single `Write` call/i,
'Step 6 must keep single-Write as the default path (no regression for non-truncating runtimes).'
);
});
});

View File

@@ -1,96 +0,0 @@
// allow-test-rule: source-text-is-the-product
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.resolve(__dirname, '..');
// Every agent that writes a large file in a single Write call must carry the
// same truncation-resilient write contract added for bug #214. OpenCode shares
// OUTPUT_TOKEN_MAX=32000 with the thinking budget (upstream opencode#18108), so
// an oversized single `write` tool call is truncated mid-payload, yielding
// `JSON Parse error: Expected '}'`, and OpenCode then doom-loops. gsd-phase-researcher
// is locked by its own bug-214 test; this locks the other large-file writers.
const WRITER_AGENTS = [
'gsd-research-synthesizer',
'gsd-planner',
'gsd-executor',
'gsd-domain-researcher',
'gsd-project-researcher',
'gsd-ui-researcher',
];
function readAgent(name) {
return fs.readFileSync(path.join(REPO_ROOT, 'agents', `${name}.md`), 'utf8');
}
describe('bug #214: large-file writer agents must survive write-tool truncation', () => {
for (const name of WRITER_AGENTS) {
describe(name, () => {
const prompt = readAgent(name);
test('keeps single-Write as the default path', () => {
assert.match(
prompt,
/in a single `Write` call/i,
`${name}: must keep single-Write as the default (no regression for non-truncating runtimes).`
);
});
test('names the truncation failure mode', () => {
assert.match(prompt, /truncat/i, `${name}: must name the truncation failure mode.`);
});
test('instructs incremental construction on large files', () => {
assert.match(
prompt,
/incrementa/i,
`${name}: must instruct incremental construction on large files.`
);
});
test('defines the continuation sentinel', () => {
assert.match(
prompt,
/<!-- gsd:write-continue -->/,
`${name}: must define the continuation sentinel for incremental writes.`
);
});
test('forbids identical retry of the oversized write (doom-loop guard)', () => {
assert.match(
prompt,
/do NOT retry the same oversized call/i,
`${name}: must forbid identical retry of the oversized write.`
);
});
test('requires Read before Edit', () => {
assert.match(
prompt,
/`Read` the file, then `Edit`/i,
`${name}: must require Read before Edit (OpenCode edit requires a prior Read).`
);
});
test('instructs removing the sentinel on the final section', () => {
assert.match(
prompt,
/no trailing sentinel/i,
`${name}: must instruct removing the sentinel on the final section.`
);
});
test('forbids silent fallback to returning content', () => {
assert.match(
prompt,
/do NOT silently fall back to returning content/i,
`${name}: must forbid silent fallback to returning content.`
);
});
});
}
});

View File

@@ -1,103 +0,0 @@
/**
* Regression tests for bug #2346
*
* Multiple GSD agents (gsd-ui-checker, gsd-planner) entered unbounded Read
* loops — re-reading the same file hundreds of times in a single run. Root
* cause: no explicit no-re-read rule or tool-budget cap in the agent prompts.
* gsd-pattern-mapper was fixed in #2312; this covers the remaining agents.
*
* Fix: add <critical_rules> block to each affected agent with:
* 1. No-re-read constraint
* 2. Large-file strategy (Grep first, then targeted offset/limit Read)
* 3. Stop-on-sufficient-evidence rule (where applicable)
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
// allow-test-rule: source-text-is-the-product
// The <critical_rules> block in agent .md files IS the fix — it is the AI instruction that
// prevents unbounded Read loops. There is no behavioral equivalent without a live LLM run.
describe('bug #2346: agent read loop guards', () => {
describe('gsd-ui-checker', () => {
const agentPath = path.join(AGENTS_DIR, 'gsd-ui-checker.md');
const content = fs.readFileSync(agentPath, 'utf-8');
test('agent file exists', () => {
assert.ok(fs.existsSync(agentPath), 'agents/gsd-ui-checker.md must exist');
});
test('has <critical_rules> block', () => {
assert.ok(
content.includes('<critical_rules>'),
'gsd-ui-checker.md must have a <critical_rules> block to prevent unbounded read loops (#2346)'
);
});
test('critical_rules contains no-re-read constraint', () => {
const rulesStart = content.indexOf('<critical_rules>');
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be complete');
const rulesBlock = content.slice(rulesStart, rulesEnd);
assert.ok(
rulesBlock.includes('re-read') || rulesBlock.includes('re read'),
'critical_rules must include a no-re-read rule'
);
});
test('critical_rules appears before success_criteria', () => {
const rulesIdx = content.indexOf('<critical_rules>');
const successIdx = content.indexOf('<success_criteria>');
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist');
assert.ok(
rulesIdx < successIdx,
'<critical_rules> must appear before <success_criteria>'
);
});
});
describe('gsd-planner', () => {
const agentPath = path.join(AGENTS_DIR, 'gsd-planner.md');
const content = fs.readFileSync(agentPath, 'utf-8');
test('agent file exists', () => {
assert.ok(fs.existsSync(agentPath), 'agents/gsd-planner.md must exist');
});
test('has <critical_rules> block', () => {
assert.ok(
content.includes('<critical_rules>'),
'gsd-planner.md must have a <critical_rules> block to prevent unbounded read loops (#2346)'
);
});
test('critical_rules contains no-re-read constraint', () => {
const rulesStart = content.indexOf('<critical_rules>');
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be complete');
const rulesBlock = content.slice(rulesStart, rulesEnd);
assert.ok(
rulesBlock.includes('re-read') || rulesBlock.includes('re read'),
'critical_rules must include a no-re-read rule'
);
});
test('critical_rules appears before success_criteria', () => {
const rulesIdx = content.indexOf('<critical_rules>');
const successIdx = content.lastIndexOf('<success_criteria>');
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both sections must exist');
assert.ok(
rulesIdx < successIdx,
'<critical_rules> must appear before <success_criteria>'
);
});
});
});

View File

@@ -1,77 +0,0 @@
/**
* Regression test for bug #2351
*
* gsd-intel-updater used hardcoded canonical paths (`agents/*.md`,
* `commands/gsd/*.md`, `hooks/*.js`, etc.) that assumed the standard
* `.claude/` runtime layout. Under a `.kilo` install, the runtime root is
* `.kilo/`, and the command directory is `command/` (not `commands/gsd/`).
* Globs against the old paths returned no results, producing semantically
* empty intel files (`"entries": {}`).
*
* Fix: add runtime layout detection and a mapping table so the agent
* resolves paths against the correct root.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-intel-updater.md');
describe('bug #2351: intel updater kilo layout support', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test('scope section includes layout detection step', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
const hasDetection =
content.includes('ls -d .kilo') ||
content.includes('Runtime layout detection') ||
content.includes('detected layout') ||
content.includes('layout detection');
assert.ok(
hasDetection,
'gsd-intel-updater.md must instruct the agent to detect the runtime layout ' +
'(.kilo vs .claude) before resolving canonical paths (#2351)'
);
});
test('scope section maps .kilo/agents path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/agents'),
'scope section must include the .kilo/agents/*.md path so agent count is correct under kilo layout'
);
});
test('scope section maps .kilo/command path (not commands/gsd)', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/command'),
'scope section must include .kilo/command path — kilo uses "command/" not "commands/gsd/"'
);
});
test('scope section maps .kilo/hooks path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/hooks'),
'scope section must include .kilo/hooks path for hook file counts'
);
});
test('scope section retains standard layout paths for .claude installs', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('agents/*.md') || content.includes('Standard `.claude` layout'),
'scope section must still document the standard .claude layout paths for non-kilo installs'
);
});
});

View File

@@ -1,112 +0,0 @@
// allow-test-rule: source-text-is-the-product
// Reads .md/.json/.yml product files whose deployed text IS what the
// runtime loads — testing text content tests the deployed contract.
/**
* 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, '..', 'gsd-core', 'workflows', 'new-project.md');
const NEW_MILESTONE_PATH = path.join(__dirname, '..', 'gsd-core', '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-project.md reports required-agent and skill-payload diagnostics separately', () => {
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
assert.ok(content.includes('required_agents_installed'),
'new-project.md must parse required_agents_installed from init JSON');
assert.ok(content.includes('missing_required_agents'),
'new-project.md must report missing required new-project agents separately');
assert.ok(content.includes('agent_skill_payloads_available'),
'new-project.md must distinguish skill payload availability from agent definitions');
assert.ok(content.includes('agents_dir'),
'new-project.md must show which agents directory was checked');
});
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

@@ -1,74 +0,0 @@
// allow-test-rule: source-text-is-the-product
// Workflow .md / agent .md / command .md / reference .md files — their text
// IS what the runtime loads. Testing text content tests the deployed contract.
// Per CONTRIBUTING.md exception matrix.
/**
* 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})`
);
});
});

View File

@@ -1,91 +0,0 @@
/**
* Regression test for bug #2686
*
* The gsd-code-fixer agent (spawned by /gsd-code-review-fix) operated directly
* against the main working tree. When it ran concurrently with a foreground
* session both processes raced for HEAD, the index, and on-disk files. The
* foreground session's next commit could land on the wrong branch (whichever
* branch the agent last checked out).
*
* Fix: the agent's working instructions must include `git worktree add` as the
* FIRST git operation, run ALL subsequent git operations inside that worktree
* path, and call `git worktree remove` for cleanup when done.
*
* This mirrors the pattern already used by every other per-issue GSD agent at
* /private/tmp/sv-<n>.
*/
'use strict';
// allow-test-rule: source-text-is-the-product
// The gsd-code-fixer agent's working instructions ARE the product — Claude
// executes them literally at runtime. Testing the text content tests the
// deployed contract: if the instruction is absent, the isolation guarantee
// is absent.
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
describe('bug-2686: review-fix agent worktree isolation', () => {
let agentContent;
before(() => {
const agentPath = path.join(__dirname, '..', 'agents', 'gsd-code-fixer.md');
assert.ok(fs.existsSync(agentPath), 'agents/gsd-code-fixer.md must exist');
agentContent = fs.readFileSync(agentPath, 'utf-8');
});
test('agent instructions include git worktree add before any branch-switching checkout or commit', () => {
const worktreePos = agentContent.indexOf('git worktree add');
assert.ok(
worktreePos !== -1,
'gsd-code-fixer.md must include a "git worktree add" instruction to isolate operations from the main working tree (#2686)'
);
// `git checkout -- {file}` is a file-restore within the worktree — safe, not a branch switch.
// The dangerous operation is `git checkout <branch>` (no leading --).
// Find the first branch-switching checkout (pattern: "git checkout " NOT followed by "--").
const branchCheckoutMatch = /git checkout (?!--)/.exec(agentContent);
if (branchCheckoutMatch) {
const branchCheckoutPos = branchCheckoutMatch.index;
assert.ok(
worktreePos < branchCheckoutPos,
'git worktree add must appear before any branch-switching git checkout in the agent instructions'
);
}
// commit command must come after worktree setup — the fixer may use
// either `git commit` directly or `gsd-sdk query commit`
const commitMatch = /(?:git commit|gsd-sdk query commit)/.exec(agentContent);
if (commitMatch) {
const commitPos = commitMatch.index;
assert.ok(
worktreePos < commitPos,
'git worktree add must appear before any commit command in the agent instructions'
);
}
});
test('agent instructions include worktree cleanup after completion', () => {
assert.ok(
agentContent.includes('git worktree remove') || agentContent.includes('worktree remove'),
'gsd-code-fixer.md must include worktree cleanup (git worktree remove) to avoid leaking tmp directories (#2686)'
);
});
test('agent instructions use a /tmp path for the worktree', () => {
// Require either a literal /tmp/sv- path or a variable assignment to /tmp/sv-
// (e.g. `wt=$(mktemp -d "/tmp/sv-..."`). Bare `$wt` or `wt=` references
// without a /tmp/sv- assignment are not sufficient.
const hasTmpWorktreePath =
/\/tmp\/sv-/.test(agentContent) ||
/\bwt\s*=\s*["']?\/tmp\/sv-/.test(agentContent);
assert.ok(
hasTmpWorktreePath,
'gsd-code-fixer.md must define a worktree variable at a /tmp/sv-... path, consistent with other GSD agents (#2686)'
);
});
});

View File

@@ -1,208 +0,0 @@
'use strict';
// allow-test-rule: source-text-is-the-product
// agents/gsd-code-fixer.md is the deployed agent definition the runtime
// loads. Parsing its bash code blocks into structured invocation records
// (extractCleanupGitInvocations + the recovery-block parsers below) IS
// testing the runtime contract — what command sequence the agent
// actually documents and executes. The .match() calls extract typed
// fields from a known-shape product file, then assertions go against
// those typed fields, not against the raw markdown text.
process.env.GSD_TEST_MODE = '1';
/**
* Bug #2990: gsd-code-fixer worktree setup fails when current branch
* is already checked out in the main repo.
*
* The original agent definition called `git worktree add "$wt" "$branch"`,
* where `$branch` was the user's currently-checked-out branch. Git refuses
* to check out the same branch in two worktrees by default, so the setup
* failed before the agent could do any work.
*
* Fix: create a NEW branch `gsd-reviewfix/${padded_phase}-$$` and attach
* the worktree to it via `git worktree add -b "$reviewfix_branch" "$wt"
* "$branch"`. The cleanup tail then fast-forwards `$branch` to
* `$reviewfix_branch` so the user's branch captures the agent's commits.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-code-fixer.md');
function parseWorktreeAddInvocations(markdown) {
// Pull `git worktree add ...` calls and classify each into structured
// records: hasNewBranchFlag (uses -b $reviewfix_branch) vs attachesToBareBranch
// ($wt $branch). Skip occurrences inside markdown inline code (backticks)
// or bash comments -- those are documentation citations of the OLD broken
// pattern, not executable instructions.
const invocations = [];
const lines = markdown.split('\n');
for (const line of lines) {
const idx = line.indexOf('git worktree add');
if (idx === -1) continue;
// Skip if inside backticks: the substring up to the match has an odd
// number of backticks, the call is inside an inline code span.
const before = line.slice(0, idx);
const backticksBefore = (before.match(/`/g) || []).length;
if (backticksBefore % 2 === 1) continue;
// Skip if the line is a bash comment (after stripping leading whitespace).
if (line.trimStart().startsWith('#')) continue;
const argstr = line.slice(idx + 'git worktree add'.length).trim();
invocations.push({
raw: argstr,
hasNewBranchFlag: /(?:^|\s)-b\s+["']?\$reviewfix_branch["']?/.test(argstr),
attachesToBareBranch: /^["']?\$wt["']?\s+["']?\$branch["']?\b/.test(argstr),
});
}
return invocations;
}
describe('Bug #2990: gsd-code-fixer worktree attaches to a NEW branch, not the user-checked-out one', () => {
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
const invocations = parseWorktreeAddInvocations(md);
test('sanity: at least one git-worktree-add invocation exists in the agent definition', () => {
assert.ok(invocations.length > 0,
'expected gsd-code-fixer.md to document at least one git worktree add invocation');
});
test('every git-worktree-add invocation uses -b $reviewfix_branch (not bare $branch)', () => {
const violations = invocations.filter(inv => inv.attachesToBareBranch);
assert.deepEqual(
violations.map(v => v.raw),
[],
`worktree-add invocations attaching to bare $branch (#2990): ${JSON.stringify(violations.map(v => v.raw), null, 2)}`,
);
});
test('the canonical setup invocation uses -b "$reviewfix_branch" "$wt" "$branch"', () => {
const setupInvocations = invocations.filter(inv => inv.hasNewBranchFlag);
assert.ok(setupInvocations.length >= 1,
`expected at least one git-worktree-add invocation with -b "$reviewfix_branch" -- found: ${JSON.stringify(invocations.map(i => i.raw), null, 2)}`);
});
});
/**
* Extract the cleanup-tail bash block from the agent .md, then parse it into
* an ordered array of `git ...` invocation records. Per-record assertions go
* against the structured records, not the raw markdown text. Anchor on the
* "Cleanup tail" header to scope to the right block (the file has multiple
* fenced bash blocks; we only want the cleanup one).
*/
function extractCleanupGitInvocations(markdown) {
// Find the cleanup tail header and the fenced bash block that follows.
const headerIdx = markdown.indexOf('**Cleanup tail (transactional');
if (headerIdx === -1) return null;
const fenceStart = markdown.indexOf('```bash', headerIdx);
if (fenceStart === -1) return null;
const fenceEnd = markdown.indexOf('```', fenceStart + '```bash'.length);
if (fenceEnd === -1) return null;
const block = markdown.slice(fenceStart + '```bash'.length, fenceEnd);
// Tokenize each non-comment, non-blank line into structured records.
const lines = block.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
const records = [];
for (const line of lines) {
// Skip occurrences inside backticks (these would be inline-code
// citations of the OLD pattern, not executable). The cleanup fenced
// block is bash, but inline backticks can still appear inside echo
// strings — guard anyway.
const ticksBefore = (line.match(/`/g) || []).length;
if (ticksBefore && ticksBefore % 2 === 1) continue;
if (!line.includes('git ') && !line.startsWith('git ')) continue;
records.push({
raw: line,
// Strip leading `git -C "..."`/`git -C $main_repo` so the verb-only
// form stays comparable across direct and -C invocations.
verb: (() => {
const m = line.match(/^git\s+(?:-C\s+\S+\s+)?(\S+)/);
return m ? m[1] : null;
})(),
// Did this line target the temp reviewfix branch by variable name?
targetsReviewfixBranch: /\$reviewfix_branch\b/.test(line) || /"\$reviewfix_branch"/.test(line),
// Is this the merge step? Captures the flag too.
isMergeFfOnly: /\bmerge\s+--ff-only\b/.test(line),
// Is this the branch-delete step?
isBranchDelete: /\bbranch\s+-D\b/.test(line),
});
}
return records;
}
describe('Bug #2990: cleanup tail fast-forwards $branch and deletes the temp branch on success', () => {
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
const records = extractCleanupGitInvocations(md);
test('cleanup tail bash block exists and is parseable', () => {
assert.notEqual(records, null, 'expected to find a "Cleanup tail" bash block in agents/gsd-code-fixer.md');
assert.ok(records.length > 0, 'expected at least one git invocation in the cleanup tail');
});
test('cleanup contains exactly one merge --ff-only against $reviewfix_branch', () => {
const merges = records.filter(r => r.isMergeFfOnly);
assert.equal(merges.length, 1, `expected exactly 1 ff-only merge, got ${merges.length}: ${JSON.stringify(merges, null, 2)}`);
assert.equal(merges[0].targetsReviewfixBranch, true, 'merge --ff-only must target $reviewfix_branch');
});
test('cleanup contains exactly one git branch -D for $reviewfix_branch', () => {
const deletes = records.filter(r => r.isBranchDelete);
assert.equal(deletes.length, 1, `expected exactly 1 branch -D, got ${deletes.length}`);
assert.equal(deletes[0].targetsReviewfixBranch, true, 'branch -D must target $reviewfix_branch');
});
test('merge --ff-only precedes branch -D in the cleanup ordering', () => {
const mergeIdx = records.findIndex(r => r.isMergeFfOnly);
const deleteIdx = records.findIndex(r => r.isBranchDelete);
assert.ok(mergeIdx >= 0 && deleteIdx >= 0);
assert.ok(mergeIdx < deleteIdx,
`merge must run before branch delete (merge=${mergeIdx}, delete=${deleteIdx}); otherwise commits could be lost on merge failure`);
});
test('recovery sentinel JSON shape records reviewfix_branch alongside worktree_path', () => {
// Find the writeFileSync call that constructs the sentinel JSON.
// Parse the JSON.stringify argument list to extract the field names.
const match = md.match(/fs\.writeFileSync\(sentinelPath,\s*JSON\.stringify\(\{([^}]+)\}/);
assert.notEqual(match, null, 'expected JSON.stringify({...}) inside the sentinel write');
const fields = match[1].split(',').map(s => s.trim().split(':')[0].trim()).filter(Boolean);
assert.ok(fields.includes('reviewfix_branch'),
`recovery sentinel must record reviewfix_branch alongside worktree_path; fields=${JSON.stringify(fields)}`);
assert.ok(fields.includes('worktree_path'),
`recovery sentinel must record worktree_path; fields=${JSON.stringify(fields)}`);
});
});
describe('Bug #2990 (#3001 CR): recovery code reads reviewfix_branch from sentinel and deletes the orphan branch', () => {
const md = fs.readFileSync(AGENT_PATH, 'utf-8');
test('recovery node script extracts reviewfix_branch from parsed sentinel', () => {
// Find the recovery `node -e '...'` block (NOT the sentinel-write one).
// Anchor on "recovery sentinel from a prior interrupted run".
const headerIdx = md.indexOf('Detected pre-existing recovery sentinel');
assert.notEqual(headerIdx, -1);
const nodeStart = md.indexOf("node -e '", headerIdx);
assert.notEqual(nodeStart, -1);
const nodeEnd = md.indexOf("' \"$sentinel\"", nodeStart);
assert.notEqual(nodeEnd, -1);
const nodeBlock = md.slice(nodeStart, nodeEnd);
// Both fields must be referenced by parsed.<field>.
assert.ok(nodeBlock.includes('parsed.reviewfix_branch'),
'recovery node script must extract parsed.reviewfix_branch from the sentinel');
assert.ok(nodeBlock.includes('parsed.worktree_path'),
'recovery node script must extract parsed.worktree_path from the sentinel');
});
test('recovery shell deletes the orphan reviewfix branch when present', () => {
// The recovery block (between sentinel detection and `rm -f "$sentinel"`)
// must call `git branch -D "$prior_branch"` (best-effort, with || true).
const sentinelIdx = md.indexOf('Detected pre-existing recovery sentinel');
const rmIdx = md.indexOf('rm -f "$sentinel"', sentinelIdx);
assert.notEqual(rmIdx, -1);
const recoveryBlock = md.slice(sentinelIdx, rmIdx);
assert.ok(/git\s+branch\s+-D\s+"\$prior_branch"/.test(recoveryBlock),
`recovery block must contain \`git branch -D "$prior_branch"\`; got: ${recoveryBlock.slice(0, 500)}`);
});
});

View File

@@ -1,46 +0,0 @@
'use strict';
// Regression guard for bug #3087.
//
// Between v1.38.3 and v1.38.4, agents/gsd-planner.md had 10 instances of
// CRITICAL/MANDATORY/ALWAYS/MUST directive emphasis systematically removed.
// The change was undocumented and conflicts with the stated intent of PR #2489
// (the sycophancy-hardening pass that shipped in the same release). This test
// enforces the restored directive language so the demotion cannot recur silently.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
let src;
try {
src = fs.readFileSync(path.join(ROOT, 'agents', 'gsd-planner.md'), 'utf8');
} catch (err) {
throw new Error(`agents/gsd-planner.md not found — was the file renamed? (${err.message})`);
}
const directives = [
{ desc: 'User Decision Fidelity heading is CRITICAL', pattern: /## CRITICAL: User Decision Fidelity/ },
{ desc: 'Never Simplify heading is CRITICAL', pattern: /## CRITICAL: Never Simplify User Decisions/ },
{ desc: 'Multi-Source Audit heading is MANDATORY', pattern: /## Multi-Source Coverage Audit \(MANDATORY in every plan set\)/ },
{ desc: 'Source audit uses "Audit ALL" imperative', pattern: /Audit ALL four source types before finalizing/ },
{ desc: 'Discovery is MANDATORY', pattern: /Discovery is MANDATORY unless/ },
{ desc: 'Split signals use ALWAYS', pattern: /\*\*ALWAYS split if:\*\*/ },
{ desc: 'requirements field doc uses MUST', pattern: /\*\*MUST\*\* list requirement IDs from ROADMAP/ },
{ desc: 'Step 0 has CRITICAL requirement ID directive', pattern: /\*\*CRITICAL:\*\* Every requirement ID MUST appear/ },
{ desc: 'Write tool directive uses ALWAYS', pattern: /\*\*ALWAYS use the Write tool to create files\*\*/ },
{ desc: 'File naming convention heading is CRITICAL', pattern: /\*\*CRITICAL — File naming convention \(enforced\):\*\*/ },
];
for (const { desc, pattern } of directives) {
test(`gsd-planner.md: ${desc}`, () => {
assert.ok(
pattern.test(src),
`Directive enforcement missing from gsd-planner.md: "${desc}" — pattern ${pattern} not found. ` +
`This language was demoted in v1.38.4 (PR #2489) without documentation, conflicting with ` +
`the sycophancy-hardening intent of that release. See bug #3087.`,
);
});
}

View File

@@ -1,117 +0,0 @@
'use strict';
// allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep
// Regression guards for bug #3097 and #3099.
//
// #3097: gsd-executor's worktree HEAD guard used `if [ -f .git ]` to detect
// worktree mode. After a Bash `cd` out of the worktree into the main repo,
// `.git` is a DIRECTORY (not a file), so the test is false and the entire
// HEAD safety block is silently skipped. Commits then land on whatever branch
// the main repo has checked out — not the per-agent worktree branch.
//
// #3099: Executor agents construct absolute paths from `pwd` captured in the
// orchestrator context (main repo root). Edit/Write calls using these paths
// resolve to the main repo, not the worktree. git commit from the worktree
// sees a clean tree; the work is silently lost or leaks to main.
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const executorSrc = fs.readFileSync(
path.join(ROOT, 'agents', 'gsd-executor.md'), 'utf8',
);
const executePhaseSrc = fs.readFileSync(
path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8',
);
describe('bug #3097: cwd-drift sentinel in gsd-executor.md', () => {
test('task_commit_protocol has cwd-drift assertion step (0a)', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
assert.ok(protocolIdx !== -1 && protocolEnd !== -1, 'task_commit_protocol block not found');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
protocol.includes('cwd') || protocol.includes('drift') || protocol.includes('gsd-spawn-toplevel'),
'task_commit_protocol missing cwd-drift assertion step — #3097 fix not applied',
);
});
test('sentinel uses git rev-parse --git-dir to detect worktree', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
protocol.includes('rev-parse --git-dir') || protocol.includes('worktrees/'),
'cwd-drift detection does not use git rev-parse --git-dir or .git/worktrees/ pattern',
);
});
test('cwd-drift check precedes HEAD assertion', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
const driftIdx = protocol.search(/cwd.drift|gsd-spawn-toplevel|drift.*assertion/i);
const headIdx = protocol.indexOf('Pre-commit HEAD safety assertion');
assert.ok(driftIdx !== -1, 'cwd-drift assertion not found');
assert.ok(headIdx !== -1, 'HEAD assertion not found');
assert.ok(driftIdx < headIdx, 'cwd-drift assertion must precede HEAD assertion (step 0a before step 0)');
});
});
describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => {
test('task_commit_protocol documents absolute-path safety', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
(protocol.includes('absolute') || protocol.includes('absolute-path')) &&
(protocol.includes('worktree') || protocol.includes('WT_ROOT')),
'task_commit_protocol missing absolute-path safety guidance — #3099 fix not applied',
);
});
test('execute-phase.md parallel_execution block references path safety', () => {
const parallelIdx = executePhaseSrc.indexOf('<parallel_execution>');
assert.ok(parallelIdx !== -1, 'parallel_execution block not found in execute-phase.md');
// Verify the worktree-path-safety.md reference is present in the execution_context
// (loaded via @ reference rather than inlined — the safe extract pattern)
assert.ok(
executePhaseSrc.includes('worktree-path-safety.md'),
'execute-phase.md does not reference worktree-path-safety.md in execution_context',
);
});
test('execute-phase prompt anchors subagent file paths to project_root before files_to_read (#280)', () => {
const filesIdx = executePhaseSrc.indexOf('<files_to_read>');
assert.ok(filesIdx !== -1, 'files_to_read block not found in execute-phase.md');
const dispatchSnippet = executePhaseSrc.slice(filesIdx, filesIdx + 1800);
assert.ok(
dispatchSnippet.includes('PROJECT_ROOT=$(git rev-parse --show-toplevel'),
'executor dispatch must compute PROJECT_ROOT in the prompt before file reads',
);
assert.ok(
dispatchSnippet.includes('${PROJECT_ROOT}/'),
'executor files_to_read paths must be anchored to ${PROJECT_ROOT}/',
);
});
test('worktree-path-safety.md reference file exists', () => {
assert.ok(
fs.existsSync(path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md')),
'gsd-core/references/worktree-path-safety.md does not exist',
);
});
test('worktree-path-safety.md contains cwd-drift and absolute-path guards', () => {
const safetySrc = fs.readFileSync(
path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md'), 'utf8',
);
assert.ok(safetySrc.includes('gsd-spawn-toplevel') || safetySrc.includes('cwd-drift'),
'worktree-path-safety.md missing cwd-drift sentinel content');
assert.ok(safetySrc.includes('WT_ROOT') || safetySrc.includes('absolute'),
'worktree-path-safety.md missing absolute-path guard content');
});
});

View File

@@ -1,187 +0,0 @@
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS
// the deployed agent instruction set. Asserting its text content tests the
// deployed behaviour contract, not internal implementation.
'use strict';
/**
* Regression tests for bug #3290.
*
* The "Runtime layout detection" block in gsd-intel-updater.md ran
* unconditionally on every project analysed, emitting:
*
* Layout detection returned "unknown" — this project is not a GSD-system
* installation (no `.claude/gsd-core/` or `.kilo/` runtime root).
*
* for every ordinary (non-GSD-framework) user project. The verdict was already
* ignored by Steps 2-6 on non-GSD projects. The block was dead-but-noisy.
*
* Fix: gate the runtime bash detection on a positive "is-this-the-framework-
* repo" check (package.json name === "@opengsd/gsd-core") so it runs ONLY when
* analysing the GSD framework's own repo, OR remove the block entirely if no
* downstream consumers exist.
*
* Group A — gating contract:
* The unconditional bash detection invocation must be absent OR wrapped in a
* framework-repo guard. A bare `ls -d .kilo ... || echo "unknown"` with no
* surrounding gate is the defect signature.
*
* Group B — no orphan consumers:
* Confirm no other agent, command, or workflow file reads/consumes the layout-
* detection verdict emitted by this block.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const AGENT_PATH = path.join(ROOT, 'agents', 'gsd-intel-updater.md');
// ─── helpers ─────────────────────────────────────────────────────────────────
/** Walk a directory recursively and return absolute paths of all .md files. */
function walkMd(dir) {
const results = [];
if (!fs.existsSync(dir)) return results;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...walkMd(abs));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
results.push(abs);
}
}
return results;
}
// ─── Group A — gating contract ───────────────────────────────────────────────
describe('bug #3290 — Group A: layout-detection block must be gated or absent', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test(
'bare unconditional detection invocation is absent — ' +
'the "ls -d .kilo ... || echo unknown" must not appear outside a framework-repo gate',
() => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
// The defect signature: the bash block runs unconditionally.
// We look for the exact shell one-liner that emits the verdict.
const bareDetectionPattern =
/ls -d \.kilo\b.*\|\|.*echo "?unknown"?/;
const hasBareDetection = bareDetectionPattern.test(content);
if (!hasBareDetection) {
// Block is fully removed — option B — pass.
return;
}
// Block is still present. Verify it is surrounded by a framework-repo gate.
// A valid gate checks package.json name or an equivalent positive signal
// that the current project IS the GSD framework's own repo.
const hasFrameworkGate =
content.includes('@opengsd/gsd-core') ||
content.includes('is-this-the-framework') ||
content.includes('framework repo') ||
content.includes('Only run') ||
/if.*package\.json.*gsd-core/i.test(content) ||
/Only.*layout detection.*GSD framework/i.test(content) ||
/Only.*layout detection.*framework/i.test(content);
assert.ok(
hasFrameworkGate,
'agents/gsd-intel-updater.md contains a bare unconditional layout-detection ' +
'bash block (`ls -d .kilo ... || echo unknown`) with no surrounding ' +
'framework-repo gate (#3290). ' +
'Either remove the block entirely, or wrap it in a check like:\n' +
' if [[ "$(jq -r \'.name // ""\' package.json 2>/dev/null)" == "@opengsd/gsd-core" ]]; then\n' +
' # ... detection block ...\n' +
' fi'
);
}
);
});
// ─── Group B — no orphan downstream consumers ────────────────────────────────
describe('bug #3290 — Group B: layout-detection verdict has no downstream consumers', () => {
const SOURCE_DIRS = [
path.join(ROOT, 'agents'),
path.join(ROOT, 'commands', 'gsd'),
path.join(ROOT, 'gsd-core', 'workflows'),
];
/**
* Lines that reference the three possible verdict values emitted by the
* detection block: "claude", "kilo", "unknown" — ONLY as the verdict output
* of the gsd-intel-updater layout detection (not general runtime references).
*
* We look for the specific phrase "Layout detection returned" which is the
* sentinel the noisy output line uses.
*/
test('no file contains "Layout detection returned" (the noisy verdict phrase)', () => {
const matches = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
const rel = path.relative(ROOT, file);
const src = fs.readFileSync(file, 'utf-8');
if (src.includes('Layout detection returned')) {
// Collect matching lines for the error message
const lines = src.split(/\r?\n/)
.map((l, i) => ({ line: l, n: i + 1 }))
.filter(({ line }) => line.includes('Layout detection returned'));
matches.push({ rel, lines });
}
}
}
assert.strictEqual(
matches.length,
0,
'Expected zero files to contain "Layout detection returned" (the noisy verdict ' +
'phrase from the gsd-intel-updater layout-detection block). Found:\n' +
matches.map(({ rel, lines }) =>
` ${rel}:\n${lines.map(({ n, line }) => ` L${n}: ${line.trim()}`).join('\n')}`
).join('\n')
);
});
test('no agent or workflow instructs reading the layout-detection verdict output', () => {
// The verdict was: echo "kilo" | echo "claude" | echo "unknown"
// If any file references "Layout detection returned unknown" as an instruction
// to consume, that would be a consumer. We verify none exist outside of
// the producing file (gsd-intel-updater.md).
const verdictConsumerPattern = /Layout detection returned.*(unknown|claude|kilo)/i;
const consumers = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
// Exclude the producer itself — it defines the message, not consumes it
if (path.basename(file) === 'gsd-intel-updater.md') continue;
const src = fs.readFileSync(file, 'utf-8');
if (verdictConsumerPattern.test(src)) {
consumers.push(path.relative(ROOT, file));
}
}
}
assert.deepStrictEqual(
consumers,
[],
'Expected no downstream consumer of the layout-detection verdict. Found:\n' +
consumers.map((f) => ` ${f}`).join('\n') +
'\nIf a consumer exists, use option A (gate) not option B (remove).'
);
});
});

View File

@@ -1,58 +0,0 @@
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
const VERIFIER_AGENT = path.join(REPO_ROOT, 'agents', 'gsd-verifier.md');
function verifierProbeContract(content) {
const sectionStart = content.indexOf('## Step 7c: Probe Execution');
const sectionEnd = content.indexOf('## Step 8:', sectionStart);
assert.notEqual(sectionStart, -1, 'verifier must define Step 7c');
assert.notEqual(sectionEnd, -1, 'verifier must close Step 7c before Step 8');
const section = content.slice(sectionStart, sectionEnd);
const codeBlocks = [...section.matchAll(/```bash\r?\n([\s\S]*?)\r?\n```/g)].map((match) => match[1].split(/\r?\n/).join('\n'));
const executionSteps = [...section.matchAll(/^\d+\.\s+(.+)$/gm)].map((match) => match[1]);
return {
title: 'Step 7c: Probe Execution',
conventionalDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('find scripts')) || null,
declaredDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('grep -R')) || null,
executionCommand: codeBlocks[1] || '',
executionSteps,
statusRows: [...section.matchAll(/^\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|[^|]+\|\s*([^|]+)\|$/gm)]
.map((match) => ({ probe: match[1], command: match[2], statuses: match[3].trim() })),
summaryClaimsRejected: section.includes('SUMMARY.md probe pass claims are not evidence'),
};
}
describe('bug #3321: gsd-verifier runs probes instead of trusting SUMMARY claims', () => {
test('verifier prompt requires direct probe discovery and execution', () => {
const content = fs.readFileSync(VERIFIER_AGENT, 'utf8');
const contract = verifierProbeContract(content);
assert.equal(contract.title, 'Step 7c: Probe Execution');
assert.equal(contract.conventionalDiscoveryCommand, "find scripts -path '*/tests/probe-*.sh' -type f 2>/dev/null | sort");
assert.equal(
contract.declaredDiscoveryCommand,
"grep -R -n -E 'probe-[^[:space:]]+\\.sh|scripts/.*/tests/probe-.*\\.sh' \"$PHASE_DIR\"/*-PLAN.md \"$PHASE_DIR\"/*-SUMMARY.md 2>/dev/null",
);
assert.deepEqual(contract.executionSteps, [
'Build the `PROBES` list from explicit PLAN declarations first; include conventional `scripts/*/tests/probe-*.sh` when the phase is a migration/tooling phase or the success criteria mention probes.',
'For every documented probe path, if the file is missing or unreadable, mark `MISSING_PROBE` and set `status: gaps_found`. Do not require the executable bit because probes run through `bash "$probe"`.',
'Run each probe from the built `PROBES` list (declared + conventional) from the repository root:',
'Exit code 0 is PASS. Any non-zero exit is FAILED and must include stdout/stderr evidence in VERIFICATION.md.',
'Do not substitute executor narration, SUMMARY.md PASS-marker counts, or a different dry-run driver command for the probe result.',
]);
assert.equal(contract.executionCommand, 'for probe in "${PROBES[@]}"; do\n timeout 30s bash "$probe"\ndone');
assert.deepEqual(contract.statusRows, [{
probe: 'scripts/.../probe-name.sh',
command: 'bash "$probe"',
statuses: 'PASS / FAILED / MISSING_PROBE',
}]);
assert.equal(contract.summaryClaimsRejected, true);
});
});

View File

@@ -1,44 +0,0 @@
// allow-test-rule: source-text-is-the-product
// Planner markdown is the deployed planning contract; these checks lock the
// exact canonical forms that downstream phase-plan-index accepts.
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
function readPlanner() {
return fs.readFileSync(PLANNER_PATH, 'utf8');
}
test('#3430: planner SUMMARY instruction uses canonical padded phase/plan form', () => {
const content = readPlanner();
assert.match(
content,
/Create `\.planning\/phases\/XX-name\/\{padded_phase\}-\{plan\}-SUMMARY\.md` when done/,
'planner must instruct executors to write SUMMARY files in canonical padded-phase form'
);
assert.doesNotMatch(
content,
/After completion, create `\.planning\/phases\/XX-name\/\{phase\}-\{plan\}-SUMMARY\.md`/,
'planner must not instruct the broken {phase}-{plan}-SUMMARY.md form'
);
});
test('#3430: planner depends_on docs show canonical in-phase plan ids', () => {
const content = readPlanner();
assert.match(
content,
/depends_on:[^\n]*Use `01-01`\/`01-01-auth-hardening`/,
'planner must document canonical depends_on examples that phase-plan-index resolves'
);
assert.doesNotMatch(
content,
/depends_on:[^\n]*01-trust\/01/,
'planner must not document phase-slug/plan-number depends_on examples as canonical'
);
});

View File

@@ -1,89 +0,0 @@
// allow-test-rule: source-text-is-the-product
// agents/*.md text IS the deployed contract — Claude Code, Codex, etc. load these
// files at runtime and surface their content to users. Testing for retired slash
// commands in this text is testing what real users will see.
/**
* Bug #3605: Stale slash command references in 5 agent files
*
* After #3042 deleted /gsd-research-phase (replaced by
* /gsd-plan-phase --research-phase <N>) and v1.40.0 consolidated /gsd-insert-phase
* into /gsd-phase insert, six occurrences survived in agents/*.md because none of
* the consolidation passes (#3029, #3044, #3131) included agents/ in their per-name
* scrub scope. scripts/fix-slash-commands.cjs lists agents/ in SEARCH_DIRS but only
* runs the /gsd- → /gsd: namespace transform, not retired-name replacement.
*
* This guard fails when any retired command name reappears in agents/*.md.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const RETIRED_COMMANDS = [
'/gsd-research-phase',
'/gsd-insert-phase',
'/gsd-add-phase',
'/gsd-remove-phase',
'/gsd-analyze-dependencies',
];
// Not the shared listAgentFiles() helper: this returns ABSOLUTE paths (consumed
// by scanForRetired below as readFileSync targets), not stripped basenames.
function listAgentFiles() {
return fs
.readdirSync(AGENTS_DIR)
.filter((name) => name.endsWith('.md'))
.map((name) => path.join(AGENTS_DIR, name));
}
function scanForRetired(filePath) {
const text = fs.readFileSync(filePath, 'utf-8');
const lines = text.split(/\r?\n/);
const hits = [];
for (let i = 0; i < lines.length; i++) {
for (const cmd of RETIRED_COMMANDS) {
const idx = lines[i].indexOf(cmd);
if (idx === -1) continue;
const next = lines[i].charCodeAt(idx + cmd.length);
// Only count if the match is a real invocation, not a prefix of a longer name.
// The next char must be a non-name char (anything outside [A-Za-z0-9-_]).
const isWordBoundary =
Number.isNaN(next) ||
!((next >= 48 && next <= 57) || // 0-9
(next >= 65 && next <= 90) || // A-Z
(next >= 97 && next <= 122) || // a-z
next === 45 || // -
next === 95); // _
if (!isWordBoundary) continue;
hits.push({ line: i + 1, cmd, text: lines[i].trim() });
}
}
return hits;
}
describe('bug #3605: agent contracts must not reference retired slash commands', () => {
const agentFiles = listAgentFiles();
test('at least one agent file is scanned (smoke)', () => {
assert.ok(agentFiles.length > 0, 'expected agents/*.md to exist');
});
for (const file of agentFiles) {
const rel = path.relative(path.join(__dirname, '..'), file);
test(`${rel} contains no retired slash commands`, () => {
const hits = scanForRetired(file);
assert.deepEqual(
hits,
[],
`${rel} contains retired command references:\n` +
hits.map((h) => ` line ${h.line}: ${h.cmd} — ${h.text}`).join('\n'),
);
});
}
});

View File

@@ -1,200 +0,0 @@
/**
* Regression tests for bug #571
*
* gsd-doc-writer in fix mode used the Write tool (whole-file replace) instead
* of the Edit tool (surgical replacement) when correcting specific failing
* claims. When the target doc was generated but not yet committed, Write could
* truncate the file to a single line with no git recovery path.
*
* Fix 1 (agent): Add Edit to the tools frontmatter and rewrite fix_mode
* instructions to mandate Edit and explicitly forbid Write on existing files.
* Fix 2 (workflow): Add a post-fix line-count guard in fix_loop that detects
* >90% shrinkage and restores the file from existing_content.
*/
'use strict';
// allow-test-rule: source-text-is-the-product
// Agent .md files are the installed AI agents — their frontmatter and body IS
// what the runtime loads. Checking text content IS checking the deployed contract.
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
const AGENT_PATH = path.join(AGENTS_DIR, 'gsd-doc-writer.md');
const WORKFLOW_PATH = path.join(WORKFLOWS_DIR, 'docs-update.md');
// ─── Agent fix: Edit in tools frontmatter ────────────────────────────────────
describe('bug #571: gsd-doc-writer agent', () => {
const content = fs.readFileSync(AGENT_PATH, 'utf-8');
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-doc-writer.md must exist');
});
test('tools frontmatter includes Edit', () => {
const toolsMatch = content.match(/^tools:\s*(.+)$/m);
assert.ok(toolsMatch, 'gsd-doc-writer.md must have a tools: frontmatter line');
assert.ok(
toolsMatch[1].includes('Edit'),
'tools: frontmatter must include Edit so fix mode can make surgical replacements (#571)'
);
});
// ─── fix_mode instructions ────────────────────────────────────────────────
describe('fix_mode block', () => {
const fixStart = content.indexOf('<fix_mode>');
const fixEnd = content.indexOf('</fix_mode>', fixStart);
assert.ok(fixStart !== -1 && fixEnd !== -1, '<fix_mode> block must be present and complete');
const fixBlock = content.slice(fixStart, fixEnd);
test('fix_mode mandates Edit for corrections', () => {
assert.ok(
fixBlock.includes('Edit'),
'fix_mode must instruct the agent to use the Edit tool for surgical corrections (#571)'
);
});
test('fix_mode explicitly forbids Write on existing files', () => {
assert.ok(
fixBlock.includes('NEVER use the Write tool') || fixBlock.includes('NEVER call Write'),
'fix_mode must explicitly forbid Write on existing files — Write replaces the whole file (#571)'
);
});
test('fix_mode mentions unrecoverable data loss risk of Write', () => {
assert.ok(
fixBlock.includes('untracked') || fixBlock.includes('context window') || fixBlock.includes('permanently destroyed'),
'fix_mode must explain WHY Write is forbidden — unrecoverable data loss for untracked files (#571)'
);
});
});
// ─── critical_rules ───────────────────────────────────────────────────────
describe('critical_rules block', () => {
const rulesStart = content.indexOf('<critical_rules>');
const rulesEnd = content.indexOf('</critical_rules>', rulesStart);
assert.ok(rulesStart !== -1 && rulesEnd !== -1, '<critical_rules> block must be present and complete');
const rulesBlock = content.slice(rulesStart, rulesEnd);
test('critical_rules forbids Write in fix mode', () => {
assert.ok(
rulesBlock.includes('fix mode') && (rulesBlock.includes('NEVER call Write') || rulesBlock.includes('NEVER use the Write')),
'critical_rules must explicitly forbid Write in fix mode (#571)'
);
});
test('critical_rules Edit rule appears before success_criteria', () => {
const rulesIdx = content.indexOf('<critical_rules>');
const successIdx = content.indexOf('<success_criteria>');
assert.ok(rulesIdx !== -1 && successIdx !== -1, 'both <critical_rules> and <success_criteria> must exist');
assert.ok(
rulesIdx < successIdx,
'<critical_rules> must appear before <success_criteria> (#571)'
);
});
});
});
// ─── Workflow fix: post-fix truncation guard in fix_loop ─────────────────────
describe('bug #571: docs-update workflow fix_loop', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
test('workflow file exists', () => {
assert.ok(fs.existsSync(WORKFLOW_PATH), 'gsd-core/workflows/docs-update.md must exist');
});
describe('fix_loop step', () => {
const loopStart = content.indexOf('<step name="fix_loop">');
const loopEnd = content.indexOf('</step>', loopStart);
assert.ok(loopStart !== -1 && loopEnd !== -1, 'fix_loop step must be present and complete');
const loopBlock = content.slice(loopStart, loopEnd);
test('fix_loop captures pre-fix line count', () => {
assert.ok(
loopBlock.includes('PRE_FIX_LINES') || loopBlock.includes('pre-fix line'),
'fix_loop must capture the pre-fix line count to detect truncation (#571)'
);
});
test('fix_loop checks post-fix line count', () => {
assert.ok(
loopBlock.includes('POST_FIX_LINES') || loopBlock.includes('post-fix line'),
'fix_loop must check the post-fix line count to detect truncation (#571)'
);
});
test('fix_loop restores file on truncation detection', () => {
assert.ok(
loopBlock.includes('Restore') || loopBlock.includes('restore'),
'fix_loop must restore the file from existing_content when truncation is detected (#571)'
);
});
test('fix_loop truncation threshold is >90% shrinkage', () => {
assert.ok(
loopBlock.includes('90%') || loopBlock.includes('10%'),
'fix_loop must use a >90% shrinkage threshold (10% of original) to detect truncation (#571)'
);
});
test('fix_loop logs a WARNING on truncation', () => {
assert.ok(
loopBlock.includes('WARNING') || loopBlock.includes('corrupted'),
'fix_loop must log a WARNING when truncation is detected and restored (#571)'
);
});
// Structural ordering: PRE check → fix agent runs → POST check → restore
// These ensure the guard is wired in the right sequence, not just present.
test('PRE_FIX_LINES is captured before POST_FIX_LINES (correct ordering)', () => {
const preIdx = loopBlock.indexOf('PRE_FIX_LINES');
const postIdx = loopBlock.indexOf('POST_FIX_LINES');
assert.ok(preIdx !== -1 && postIdx !== -1, 'both PRE_FIX_LINES and POST_FIX_LINES must be present (#571)');
assert.ok(
preIdx < postIdx,
'PRE_FIX_LINES must appear before POST_FIX_LINES — pre-capture must happen before post-check (#571)'
);
});
test('restore instruction appears after POST_FIX_LINES check (correct ordering)', () => {
const postIdx = loopBlock.indexOf('POST_FIX_LINES');
// Find the restore instruction — it follows the threshold comparison
const restoreIdx = loopBlock.indexOf('existing_content', postIdx);
assert.ok(
restoreIdx !== -1 && restoreIdx > postIdx,
'restore-from-existing_content instruction must appear after the POST_FIX_LINES check (#571)'
);
});
test('fix_loop doc path is quoted in shell snippets', () => {
// Unquoted paths break on filenames with spaces or shell metacharacters.
// Verify the bash snippets use quoted "{doc_path}" not bare {doc_path}.
assert.ok(
loopBlock.includes('< "{doc_path}"') || loopBlock.includes("<\"{doc_path}\""),
'shell redirections must quote {doc_path} to handle paths with spaces (#571)'
);
});
test('corrupted doc is still re-verified (not silently skipped)', () => {
// The restored doc must be included in step 2 re-verification so its
// failures are counted and reported. It should only be excluded from
// receiving another fix attempt, not from verification.
const restoreIdx = loopBlock.indexOf('existing_content', loopBlock.indexOf('POST_FIX_LINES'));
const reVerifyIdx = loopBlock.indexOf('re-verify', restoreIdx);
assert.ok(
reVerifyIdx !== -1,
'fix_loop must include re-verification after truncation restore (corrupted docs still have failures) (#571)'
);
});
});
});

View File

@@ -1,100 +0,0 @@
'use strict';
/**
* Tests for #2427 — prompt-level sycophancy hardening of audit-class agents.
* Verifies the four required changes are present in each agent file:
* 1. Third-person framing (no "You are a GSD X" opening in <role>)
* 2. FORCE adversarial stance block
* 3. Explicit failure modes list
* 4. BLOCKER/WARNING classification requirement
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const AGENTS_DIR = path.join(__dirname, '../agents');
const AUDIT_AGENTS = [
'gsd-plan-checker.md',
'gsd-code-reviewer.md',
'gsd-security-auditor.md',
'gsd-verifier.md',
'gsd-eval-auditor.md',
'gsd-nyquist-auditor.md',
'gsd-ui-auditor.md',
'gsd-integration-checker.md',
'gsd-doc-verifier.md',
];
function readAgent(agentsDir, filename) {
return fs.readFileSync(path.join(agentsDir, filename), 'utf-8');
}
function extractRole(content) {
const match = content.match(/<role>([\s\S]*?)<\/role>/);
return match ? match[1] : '';
}
describe('enh-2427 — sycophancy hardening: audit-class agents', () => {
for (const filename of AUDIT_AGENTS) {
const label = filename.replace('.md', '');
describe(label, () => {
let content;
let role;
test('file is readable', () => {
content = readAgent(AGENTS_DIR, filename);
role = extractRole(content);
assert.ok(content.length > 0, `${filename} should not be empty`);
});
test('(1) third-person framing — <role> does not open with "You are a GSD"', () => {
content = content || readAgent(AGENTS_DIR, filename);
role = role || extractRole(content);
const firstSentence = role.trim().slice(0, 80);
assert.ok(
!firstSentence.startsWith('You are a GSD'),
`${filename}: <role> must not open with "You are a GSD" — use third-person submission framing. Got: "${firstSentence}"`
);
});
test('(2) FORCE adversarial stance — <adversarial_stance> block present', () => {
content = content || readAgent(AGENTS_DIR, filename);
assert.ok(
content.includes('<adversarial_stance>'),
`${filename}: must contain <adversarial_stance> block`
);
assert.ok(
content.includes('FORCE stance'),
`${filename}: <adversarial_stance> must contain "FORCE stance"`
);
});
test('(3) explicit failure modes list present', () => {
content = content || readAgent(AGENTS_DIR, filename);
assert.ok(
content.includes('failure modes'),
`${filename}: must contain "failure modes" section in <adversarial_stance>`
);
});
test('(4) BLOCKER/WARNING classification requirement present', () => {
content = content || readAgent(AGENTS_DIR, filename);
assert.ok(
content.includes('**BLOCKER**'),
`${filename}: must define BLOCKER classification in <adversarial_stance>`
);
assert.ok(
content.includes('**WARNING**'),
`${filename}: must define WARNING classification in <adversarial_stance>`
);
});
});
}
});
// sdk/prompts/agents/ was removed in 377a6d2 — SDK now loads installed agents directly.

View File

@@ -1,134 +0,0 @@
/**
* Enhancement #2500: gsd-codebase-mapper (arch focus) rich architecture output
*
* The codebase/ARCHITECTURE.md produced by gsd-codebase-mapper was a sparse
* structural inventory — file listings and module relationships. After a major
* refactor, research/ARCHITECTURE.md (created at /gsd-new-project) goes stale
* with no refresh command. This enhancement enriches the codebase mapper's
* arch-focus template to match the richness of the research version:
* - ASCII system overview diagram
* - Data flow traces with numbered steps and code references
* - Component responsibility table (component → responsibility → file)
* - Critical architectural constraints
* - Anti-patterns specific to the codebase
* - <!-- refreshed: {date} --> marker at top (maintainer request)
*
* The agent's template text IS what the runtime executes, so testing
* the template content directly tests the deployed contract.
*/
'use strict';
// allow-test-rule: source-text-is-the-product
// The gsd-codebase-mapper ARCHITECTURE.md template is the instruction set
// executed by the LLM at runtime. Testing its text content tests whether the
// deployed agent will produce rich architecture docs as required by #2500.
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-codebase-mapper.md');
describe('enh-2500: gsd-codebase-mapper arch focus — rich architecture output', () => {
let agentContent;
let archTemplate;
before(() => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-codebase-mapper.md must exist');
agentContent = fs.readFileSync(AGENT_PATH, 'utf-8');
// Isolate the ARCHITECTURE.md template section from the agent file.
// End boundary is the STRUCTURE.md Template heading that immediately follows it.
const archStart = agentContent.indexOf('## ARCHITECTURE.md Template (arch focus)');
assert.ok(archStart !== -1, 'agent must contain an ARCHITECTURE.md Template (arch focus) section');
const archEnd = agentContent.indexOf('## STRUCTURE.md Template (arch focus)', archStart + 1);
archTemplate = archEnd !== -1
? agentContent.slice(archStart, archEnd)
: agentContent.slice(archStart);
});
test('template includes a refreshed date marker', () => {
assert.ok(
archTemplate.includes('<!-- refreshed:') || archTemplate.includes('refreshed:'),
'ARCHITECTURE.md template must include a <!-- refreshed: {date} --> marker so users can see when the doc was last generated (#2500 maintainer requirement)'
);
});
test('template includes an ASCII system overview diagram', () => {
// ASCII diagrams use box-drawing characters or at minimum ┌/└/│/─ or +/|/-
const hasAsciiDiagram =
archTemplate.includes('┌') ||
archTemplate.includes('└') ||
archTemplate.includes('│') ||
archTemplate.includes('+--') ||
archTemplate.includes('+-') ||
archTemplate.includes('→') ||
archTemplate.includes('↓') ||
archTemplate.includes('↑');
assert.ok(
hasAsciiDiagram,
'ARCHITECTURE.md template must include an ASCII system overview diagram (box-drawing characters or flow arrows) as required by #2500'
);
});
test('template includes System Overview section header', () => {
assert.ok(
archTemplate.includes('System Overview') || archTemplate.includes('system overview'),
'ARCHITECTURE.md template must include a "System Overview" section for the ASCII diagram (#2500)'
);
});
test('template includes a component responsibility table with required columns', () => {
// Must have a markdown table with component, responsibility, and file columns
const hasComponentCol =
archTemplate.includes('Component') || archTemplate.includes('component');
const hasResponsibilityCol =
archTemplate.includes('Responsibility') || archTemplate.includes('responsibility');
const hasFileCol =
archTemplate.includes('File') || archTemplate.includes('file');
assert.ok(
hasComponentCol && hasResponsibilityCol && hasFileCol,
'ARCHITECTURE.md template must include a component responsibility table with Component, Responsibility, and File columns (#2500)'
);
});
test('template includes data flow traces with numbered steps', () => {
const hasPrimaryRequestPath = /###\s+Primary Request Path/i.test(archTemplate);
// [^\n]+ + \r?\n is CRLF-tolerant: .+ doesn't match \r in JS regex by
// default, so \r before the literal \n in CRLF content kills the match.
const hasThreeNumberedSteps = /^\s*1\.[^\n]+\r?\n\s*2\.[^\n]+\r?\n\s*3\./m.test(archTemplate);
const hasFileLineRefs = /\(`\[.*:(?:line|\d+)\]`\)/.test(archTemplate);
assert.ok(
hasPrimaryRequestPath && hasThreeNumberedSteps && hasFileLineRefs,
'ARCHITECTURE.md template must include a "Primary Request Path" section with numbered steps and file:line references (#2500)'
);
});
test('template includes architectural constraints section', () => {
const hasConstraints =
/##\s+Architectural Constraints/i.test(archTemplate) &&
/\bThreading\b/.test(archTemplate) &&
/\bGlobal state\b/i.test(archTemplate) &&
/\bCircular imports\b/i.test(archTemplate);
assert.ok(
hasConstraints,
'ARCHITECTURE.md template must include an "Architectural Constraints" section with Threading, Global state, and Circular imports categories (#2500)'
);
});
test('template includes anti-patterns section', () => {
assert.ok(
archTemplate.includes('Anti-pattern') ||
archTemplate.includes('Anti-Pattern') ||
archTemplate.includes('anti-pattern'),
'ARCHITECTURE.md template must include an anti-patterns section specific to the codebase (#2500)'
);
});
});

View File

@@ -96,3 +96,130 @@ describe('gsd-executor — state.* calls use the named-only router form (#1863 r
}
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3097-3099-executor-worktree-path-safety.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3097-3099-executor-worktree-path-safety (consolidation epic #1969 B7 #1976)", () => {
'use strict';
// allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep (see #3097)
// Regression guards for bug #3097 and #3099.
//
// #3097: gsd-executor's worktree HEAD guard used `if [ -f .git ]` to detect
// worktree mode. After a Bash `cd` out of the worktree into the main repo,
// `.git` is a DIRECTORY (not a file), so the test is false and the entire
// HEAD safety block is silently skipped. Commits then land on whatever branch
// the main repo has checked out — not the per-agent worktree branch.
//
// #3099: Executor agents construct absolute paths from `pwd` captured in the
// orchestrator context (main repo root). Edit/Write calls using these paths
// resolve to the main repo, not the worktree. git commit from the worktree
// sees a clean tree; the work is silently lost or leaks to main.
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const executorSrc = fs.readFileSync(
path.join(ROOT, 'agents', 'gsd-executor.md'), 'utf8',
);
const executePhaseSrc = fs.readFileSync(
path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8',
);
describe('bug #3097: cwd-drift sentinel in gsd-executor.md', () => {
test('task_commit_protocol has cwd-drift assertion step (0a)', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
assert.ok(protocolIdx !== -1 && protocolEnd !== -1, 'task_commit_protocol block not found');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
protocol.includes('cwd') || protocol.includes('drift') || protocol.includes('gsd-spawn-toplevel'),
'task_commit_protocol missing cwd-drift assertion step — #3097 fix not applied',
);
});
test('sentinel uses git rev-parse --git-dir to detect worktree', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
protocol.includes('rev-parse --git-dir') || protocol.includes('worktrees/'),
'cwd-drift detection does not use git rev-parse --git-dir or .git/worktrees/ pattern',
);
});
test('cwd-drift check precedes HEAD assertion', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
const driftIdx = protocol.search(/cwd.drift|gsd-spawn-toplevel|drift.*assertion/i);
const headIdx = protocol.indexOf('Pre-commit HEAD safety assertion');
assert.ok(driftIdx !== -1, 'cwd-drift assertion not found');
assert.ok(headIdx !== -1, 'HEAD assertion not found');
assert.ok(driftIdx < headIdx, 'cwd-drift assertion must precede HEAD assertion (step 0a before step 0)');
});
});
describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => {
test('task_commit_protocol documents absolute-path safety', () => {
const protocolIdx = executorSrc.indexOf('<task_commit_protocol>');
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
const protocol = executorSrc.slice(protocolIdx, protocolEnd);
assert.ok(
(protocol.includes('absolute') || protocol.includes('absolute-path')) &&
(protocol.includes('worktree') || protocol.includes('WT_ROOT')),
'task_commit_protocol missing absolute-path safety guidance — #3099 fix not applied',
);
});
test('execute-phase.md parallel_execution block references path safety', () => {
const parallelIdx = executePhaseSrc.indexOf('<parallel_execution>');
assert.ok(parallelIdx !== -1, 'parallel_execution block not found in execute-phase.md');
// Verify the worktree-path-safety.md reference is present in the execution_context
// (loaded via @ reference rather than inlined — the safe extract pattern)
assert.ok(
executePhaseSrc.includes('worktree-path-safety.md'),
'execute-phase.md does not reference worktree-path-safety.md in execution_context',
);
});
test('execute-phase prompt anchors subagent file paths to project_root before files_to_read (#280)', () => {
const filesIdx = executePhaseSrc.indexOf('<files_to_read>');
assert.ok(filesIdx !== -1, 'files_to_read block not found in execute-phase.md');
const dispatchSnippet = executePhaseSrc.slice(filesIdx, filesIdx + 1800);
assert.ok(
dispatchSnippet.includes('PROJECT_ROOT=$(git rev-parse --show-toplevel'),
'executor dispatch must compute PROJECT_ROOT in the prompt before file reads',
);
assert.ok(
dispatchSnippet.includes('${PROJECT_ROOT}/'),
'executor files_to_read paths must be anchored to ${PROJECT_ROOT}/',
);
});
test('worktree-path-safety.md reference file exists', () => {
assert.ok(
fs.existsSync(path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md')),
'gsd-core/references/worktree-path-safety.md does not exist',
);
});
test('worktree-path-safety.md contains cwd-drift and absolute-path guards', () => {
const safetySrc = fs.readFileSync(
path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md'), 'utf8',
);
assert.ok(safetySrc.includes('gsd-spawn-toplevel') || safetySrc.includes('cwd-drift'),
'worktree-path-safety.md missing cwd-drift sentinel content');
assert.ok(safetySrc.includes('WT_ROOT') || safetySrc.includes('absolute'),
'worktree-path-safety.md missing absolute-path guard content');
});
});
});
}

View File

@@ -1263,3 +1263,286 @@ describe('#3258: no stale /gsd-intel slash-command references in product source
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2351-intel-kilo-layout.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2351-intel-kilo-layout (consolidation epic #1969 B7 #1976)", () => {
/**
* Regression test for bug #2351
*
* gsd-intel-updater used hardcoded canonical paths (`agents/*.md`,
* `commands/gsd/*.md`, `hooks/*.js`, etc.) that assumed the standard
* `.claude/` runtime layout. Under a `.kilo` install, the runtime root is
* `.kilo/`, and the command directory is `command/` (not `commands/gsd/`).
* Globs against the old paths returned no results, producing semantically
* empty intel files (`"entries": {}`).
*
* Fix: add runtime layout detection and a mapping table so the agent
* resolves paths against the correct root.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENT_PATH = path.join(__dirname, '..', 'agents', 'gsd-intel-updater.md');
describe('bug #2351: intel updater kilo layout support', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test('scope section includes layout detection step', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
const hasDetection =
content.includes('ls -d .kilo') ||
content.includes('Runtime layout detection') ||
content.includes('detected layout') ||
content.includes('layout detection');
assert.ok(
hasDetection,
'gsd-intel-updater.md must instruct the agent to detect the runtime layout ' +
'(.kilo vs .claude) before resolving canonical paths (#2351)'
);
});
test('scope section maps .kilo/agents path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/agents'),
'scope section must include the .kilo/agents/*.md path so agent count is correct under kilo layout'
);
});
test('scope section maps .kilo/command path (not commands/gsd)', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/command'),
'scope section must include .kilo/command path — kilo uses "command/" not "commands/gsd/"'
);
});
test('scope section maps .kilo/hooks path', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('.kilo/hooks'),
'scope section must include .kilo/hooks path for hook file counts'
);
});
test('scope section retains standard layout paths for .claude installs', () => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
assert.ok(
content.includes('agents/*.md') || content.includes('Standard `.claude` layout'),
'scope section must still document the standard .claude layout paths for non-kilo installs'
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3290-intel-updater-layout-block.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3290-intel-updater-layout-block (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product — agents/gsd-intel-updater.md IS (see #3290)
// the deployed agent instruction set. Asserting its text content tests the
// deployed behaviour contract, not internal implementation.
'use strict';
/**
* Regression tests for bug #3290.
*
* The "Runtime layout detection" block in gsd-intel-updater.md ran
* unconditionally on every project analysed, emitting:
*
* Layout detection returned "unknown" — this project is not a GSD-system
* installation (no `.claude/gsd-core/` or `.kilo/` runtime root).
*
* for every ordinary (non-GSD-framework) user project. The verdict was already
* ignored by Steps 2-6 on non-GSD projects. The block was dead-but-noisy.
*
* Fix: gate the runtime bash detection on a positive "is-this-the-framework-
* repo" check (package.json name === "@opengsd/gsd-core") so it runs ONLY when
* analysing the GSD framework's own repo, OR remove the block entirely if no
* downstream consumers exist.
*
* Group A — gating contract:
* The unconditional bash detection invocation must be absent OR wrapped in a
* framework-repo guard. A bare `ls -d .kilo ... || echo "unknown"` with no
* surrounding gate is the defect signature.
*
* Group B — no orphan consumers:
* Confirm no other agent, command, or workflow file reads/consumes the layout-
* detection verdict emitted by this block.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const AGENT_PATH = path.join(ROOT, 'agents', 'gsd-intel-updater.md');
// ─── helpers ─────────────────────────────────────────────────────────────────
/** Walk a directory recursively and return absolute paths of all .md files. */
function walkMd(dir) {
const results = [];
if (!fs.existsSync(dir)) return results;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...walkMd(abs));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
results.push(abs);
}
}
return results;
}
// ─── Group A — gating contract ───────────────────────────────────────────────
describe('bug #3290 — Group A: layout-detection block must be gated or absent', () => {
let content;
test('agent file exists', () => {
assert.ok(fs.existsSync(AGENT_PATH), 'agents/gsd-intel-updater.md must exist');
content = fs.readFileSync(AGENT_PATH, 'utf-8');
});
test(
'bare unconditional detection invocation is absent — ' +
'the "ls -d .kilo ... || echo unknown" must not appear outside a framework-repo gate',
() => {
content = content || fs.readFileSync(AGENT_PATH, 'utf-8');
// The defect signature: the bash block runs unconditionally.
// We look for the exact shell one-liner that emits the verdict.
const bareDetectionPattern =
/ls -d \.kilo\b.*\|\|.*echo "?unknown"?/;
const hasBareDetection = bareDetectionPattern.test(content);
if (!hasBareDetection) {
// Block is fully removed — option B — pass.
return;
}
// Block is still present. Verify it is surrounded by a framework-repo gate.
// A valid gate checks package.json name or an equivalent positive signal
// that the current project IS the GSD framework's own repo.
const hasFrameworkGate =
content.includes('@opengsd/gsd-core') ||
content.includes('is-this-the-framework') ||
content.includes('framework repo') ||
content.includes('Only run') ||
/if.*package\.json.*gsd-core/i.test(content) ||
/Only.*layout detection.*GSD framework/i.test(content) ||
/Only.*layout detection.*framework/i.test(content);
assert.ok(
hasFrameworkGate,
'agents/gsd-intel-updater.md contains a bare unconditional layout-detection ' +
'bash block (`ls -d .kilo ... || echo unknown`) with no surrounding ' +
'framework-repo gate (#3290). ' +
'Either remove the block entirely, or wrap it in a check like:\n' +
' if [[ "$(jq -r \'.name // ""\' package.json 2>/dev/null)" == "@opengsd/gsd-core" ]]; then\n' +
' # ... detection block ...\n' +
' fi'
);
}
);
});
// ─── Group B — no orphan downstream consumers ────────────────────────────────
describe('bug #3290 — Group B: layout-detection verdict has no downstream consumers', () => {
const SOURCE_DIRS = [
path.join(ROOT, 'agents'),
path.join(ROOT, 'commands', 'gsd'),
path.join(ROOT, 'gsd-core', 'workflows'),
];
/**
* Lines that reference the three possible verdict values emitted by the
* detection block: "claude", "kilo", "unknown" — ONLY as the verdict output
* of the gsd-intel-updater layout detection (not general runtime references).
*
* We look for the specific phrase "Layout detection returned" which is the
* sentinel the noisy output line uses.
*/
test('no file contains "Layout detection returned" (the noisy verdict phrase)', () => {
const matches = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
const rel = path.relative(ROOT, file);
const src = fs.readFileSync(file, 'utf-8');
if (src.includes('Layout detection returned')) {
// Collect matching lines for the error message
const lines = src.split(/\r?\n/)
.map((l, i) => ({ line: l, n: i + 1 }))
.filter(({ line }) => line.includes('Layout detection returned'));
matches.push({ rel, lines });
}
}
}
assert.strictEqual(
matches.length,
0,
'Expected zero files to contain "Layout detection returned" (the noisy verdict ' +
'phrase from the gsd-intel-updater layout-detection block). Found:\n' +
matches.map(({ rel, lines }) =>
` ${rel}:\n${lines.map(({ n, line }) => ` L${n}: ${line.trim()}`).join('\n')}`
).join('\n')
);
});
test('no agent or workflow instructs reading the layout-detection verdict output', () => {
// The verdict was: echo "kilo" | echo "claude" | echo "unknown"
// If any file references "Layout detection returned unknown" as an instruction
// to consume, that would be a consumer. We verify none exist outside of
// the producing file (gsd-intel-updater.md).
const verdictConsumerPattern = /Layout detection returned.*(unknown|claude|kilo)/i;
const consumers = [];
for (const dir of SOURCE_DIRS) {
const files = walkMd(dir);
for (const file of files) {
// Exclude the producer itself — it defines the message, not consumes it
if (path.basename(file) === 'gsd-intel-updater.md') continue;
const src = fs.readFileSync(file, 'utf-8');
if (verdictConsumerPattern.test(src)) {
consumers.push(path.relative(ROOT, file));
}
}
}
assert.deepStrictEqual(
consumers,
[],
'Expected no downstream consumer of the layout-detection verdict. Found:\n' +
consumers.map((f) => ` ${f}`).join('\n') +
'\nIf a consumer exists, use option A (gate) not option B (remove).'
);
});
});
});
}

View File

@@ -489,3 +489,196 @@ describe('bug #3805: fast.md log_to_state must be schema-aware', () => {
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2421-planner-grep-gate-hygiene.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2421-planner-grep-gate-hygiene (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product (see #2421)
// Workflow .md / agent .md / command .md / reference .md files — their text
// IS what the runtime loads. Testing text content tests the deployed contract.
// Per CONTRIBUTING.md exception matrix.
/**
* 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})`
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3087-planner-directive-language.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3087-planner-directive-language (consolidation epic #1969 B7 #1976)", () => {
'use strict';
// Regression guard for bug #3087.
//
// Between v1.38.3 and v1.38.4, agents/gsd-planner.md had 10 instances of
// CRITICAL/MANDATORY/ALWAYS/MUST directive emphasis systematically removed.
// The change was undocumented and conflicts with the stated intent of PR #2489
// (the sycophancy-hardening pass that shipped in the same release). This test
// enforces the restored directive language so the demotion cannot recur silently.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
let src;
try {
src = fs.readFileSync(path.join(ROOT, 'agents', 'gsd-planner.md'), 'utf8');
} catch (err) {
throw new Error(`agents/gsd-planner.md not found — was the file renamed? (${err.message})`);
}
const directives = [
{ desc: 'User Decision Fidelity heading is CRITICAL', pattern: /## CRITICAL: User Decision Fidelity/ },
{ desc: 'Never Simplify heading is CRITICAL', pattern: /## CRITICAL: Never Simplify User Decisions/ },
{ desc: 'Multi-Source Audit heading is MANDATORY', pattern: /## Multi-Source Coverage Audit \(MANDATORY in every plan set\)/ },
{ desc: 'Source audit uses "Audit ALL" imperative', pattern: /Audit ALL four source types before finalizing/ },
{ desc: 'Discovery is MANDATORY', pattern: /Discovery is MANDATORY unless/ },
{ desc: 'Split signals use ALWAYS', pattern: /\*\*ALWAYS split if:\*\*/ },
{ desc: 'requirements field doc uses MUST', pattern: /\*\*MUST\*\* list requirement IDs from ROADMAP/ },
{ desc: 'Step 0 has CRITICAL requirement ID directive', pattern: /\*\*CRITICAL:\*\* Every requirement ID MUST appear/ },
{ desc: 'Write tool directive uses ALWAYS', pattern: /\*\*ALWAYS use the Write tool to create files\*\*/ },
{ desc: 'File naming convention heading is CRITICAL', pattern: /\*\*CRITICAL — File naming convention \(enforced\):\*\*/ },
];
for (const { desc, pattern } of directives) {
test(`gsd-planner.md: ${desc}`, () => {
assert.ok(
pattern.test(src),
`Directive enforcement missing from gsd-planner.md: "${desc}" — pattern ${pattern} not found. ` +
`This language was demoted in v1.38.4 (PR #2489) without documentation, conflicting with ` +
`the sycophancy-hardening intent of that release. See bug #3087.`,
);
});
}
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3430-planner-phase-contract.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3430-planner-phase-contract (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product (see #3430)
// Planner markdown is the deployed planning contract; these checks lock the
// exact canonical forms that downstream phase-plan-index accepts.
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
function readPlanner() {
return fs.readFileSync(PLANNER_PATH, 'utf8');
}
test('#3430: planner SUMMARY instruction uses canonical padded phase/plan form', () => {
const content = readPlanner();
assert.match(
content,
/Create `\.planning\/phases\/XX-name\/\{padded_phase\}-\{plan\}-SUMMARY\.md` when done/,
'planner must instruct executors to write SUMMARY files in canonical padded-phase form'
);
assert.doesNotMatch(
content,
/After completion, create `\.planning\/phases\/XX-name\/\{phase\}-\{plan\}-SUMMARY\.md`/,
'planner must not instruct the broken {phase}-{plan}-SUMMARY.md form'
);
});
test('#3430: planner depends_on docs show canonical in-phase plan ids', () => {
const content = readPlanner();
assert.match(
content,
/depends_on:[^\n]*Use `01-01`\/`01-01-auth-hardening`/,
'planner must document canonical depends_on examples that phase-plan-index resolves'
);
assert.doesNotMatch(
content,
/depends_on:[^\n]*01-trust\/01/,
'planner must not document phase-slug/plan-number depends_on examples as canonical'
);
});
});
}

View File

@@ -311,3 +311,124 @@ describe('bug #222 recurrence: orchestrator self-heals when synthesizer returns
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2419-project-researcher-agent.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2419-project-researcher-agent (consolidation epic #1969 B7 #1976)", () => {
// allow-test-rule: source-text-is-the-product (see #2419)
// Reads .md/.json/.yml product files whose deployed text IS what the
// runtime loads — testing text content tests the deployed contract.
/**
* 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, '..', 'gsd-core', 'workflows', 'new-project.md');
const NEW_MILESTONE_PATH = path.join(__dirname, '..', 'gsd-core', '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-project.md reports required-agent and skill-payload diagnostics separately', () => {
const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8');
assert.ok(content.includes('required_agents_installed'),
'new-project.md must parse required_agents_installed from init JSON');
assert.ok(content.includes('missing_required_agents'),
'new-project.md must report missing required new-project agents separately');
assert.ok(content.includes('agent_skill_payloads_available'),
'new-project.md must distinguish skill payload availability from agent definitions');
assert.ok(content.includes('agents_dir'),
'new-project.md must show which agents directory was checked');
});
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

@@ -127,3 +127,71 @@ test('shipped workflow flags behavior-unverified truths even on infrastructure p
test('standalone template per-truth guideline respects gaps_found precedence', () => {
assert.match(standalone, /becomes `human_needed`[\s\S]{0,80}?gaps_found/i);
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3321-verifier-runs-probes.test.cjs — consolidation epic #1969 (B7 #1976)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3321-verifier-runs-probes (consolidation epic #1969 B7 #1976)", () => {
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
const VERIFIER_AGENT = path.join(REPO_ROOT, 'agents', 'gsd-verifier.md');
function verifierProbeContract(content) {
const sectionStart = content.indexOf('## Step 7c: Probe Execution');
const sectionEnd = content.indexOf('## Step 8:', sectionStart);
assert.notEqual(sectionStart, -1, 'verifier must define Step 7c');
assert.notEqual(sectionEnd, -1, 'verifier must close Step 7c before Step 8');
const section = content.slice(sectionStart, sectionEnd);
const codeBlocks = [...section.matchAll(/```bash\r?\n([\s\S]*?)\r?\n```/g)].map((match) => match[1].split(/\r?\n/).join('\n'));
const executionSteps = [...section.matchAll(/^\d+\.\s+(.+)$/gm)].map((match) => match[1]);
return {
title: 'Step 7c: Probe Execution',
conventionalDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('find scripts')) || null,
declaredDiscoveryCommand: codeBlocks[0]?.split('\n').find((line) => line.startsWith('grep -R')) || null,
executionCommand: codeBlocks[1] || '',
executionSteps,
statusRows: [...section.matchAll(/^\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|[^|]+\|\s*([^|]+)\|$/gm)]
.map((match) => ({ probe: match[1], command: match[2], statuses: match[3].trim() })),
summaryClaimsRejected: section.includes('SUMMARY.md probe pass claims are not evidence'),
};
}
describe('bug #3321: gsd-verifier runs probes instead of trusting SUMMARY claims', () => {
test('verifier prompt requires direct probe discovery and execution', () => {
const content = fs.readFileSync(VERIFIER_AGENT, 'utf8');
const contract = verifierProbeContract(content);
assert.equal(contract.title, 'Step 7c: Probe Execution');
assert.equal(contract.conventionalDiscoveryCommand, "find scripts -path '*/tests/probe-*.sh' -type f 2>/dev/null | sort");
assert.equal(
contract.declaredDiscoveryCommand,
"grep -R -n -E 'probe-[^[:space:]]+\\.sh|scripts/.*/tests/probe-.*\\.sh' \"$PHASE_DIR\"/*-PLAN.md \"$PHASE_DIR\"/*-SUMMARY.md 2>/dev/null",
);
assert.deepEqual(contract.executionSteps, [
'Build the `PROBES` list from explicit PLAN declarations first; include conventional `scripts/*/tests/probe-*.sh` when the phase is a migration/tooling phase or the success criteria mention probes.',
'For every documented probe path, if the file is missing or unreadable, mark `MISSING_PROBE` and set `status: gaps_found`. Do not require the executable bit because probes run through `bash "$probe"`.',
'Run each probe from the built `PROBES` list (declared + conventional) from the repository root:',
'Exit code 0 is PASS. Any non-zero exit is FAILED and must include stdout/stderr evidence in VERIFICATION.md.',
'Do not substitute executor narration, SUMMARY.md PASS-marker counts, or a different dry-run driver command for the probe result.',
]);
assert.equal(contract.executionCommand, 'for probe in "${PROBES[@]}"; do\n timeout 30s bash "$probe"\ndone');
assert.deepEqual(contract.statusRows, [{
probe: 'scripts/.../probe-name.sh',
command: 'bash "$probe"',
statuses: 'PASS / FAILED / MISSING_PROBE',
}]);
assert.equal(contract.summaryClaimsRejected, true);
});
});
});
}