fix(#1041): route all source agents through the canonical multi-runtime gsd-tools resolver (#1045)

* fix(#1041): route all source agents through the canonical multi-runtime gsd-tools resolver

Source agents/*.md (gsd-planner, gsd-executor, gsd-verifier, gsd-plan-checker,
gsd-intel-updater, gsd-debugger, …) called bare "gsd-tools …" in shell blocks.
On a shim-only install — where gsd-tools.cjs exists under the runtime home but
gsd-tools is NOT on PATH — those calls fail with "command not found" and the
agent silently skips init/state/validate/commit ceremony, deferring to the
orchestrator or bypassing GSD bookkeeping entirely.

were never migrated, so it persisted on Claude Code and every other runtime that
consumes the source agents directly. Only gsd-phase-researcher.md carried a
resolver — and a stale, claude-only truncated one.

Fix (all runtimes):
- Inject the canonical multi-runtime gsd_run preamble (byte-equal to
  _runtime-launcher.snippet.sh — claude/codex/cursor/gemini/copilot/windsurf/
  augment/trae/qwen/cline/opencode/kilo/hermes/antigravity homes) at the top of
  the first gsd_run block of all 12 gsd-tools-calling agents, and rewrite every
  command-position bare gsd-tools to gsd_run.
- Upgrade gsd-phase-researcher.md's stale resolver to the canonical one.
- Extend scripts/sync-runtime-launcher.cjs to maintain agents/ in parity (the
  sync caught and corrected a mis-placed preamble during development).
- Extend the bare-gsd-tools (#2851) and launcher-parity (#373) regression guards
  to agents/ so no runtime can silently regress.

Closes #1041

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1041): backfill changeset PR number to 1045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-11 11:42:59 -04:00
committed by GitHub
parent e4dfa6b9ea
commit 1fab2e10ba
20 changed files with 426 additions and 256 deletions

View File

@@ -119,6 +119,37 @@ function lineHasBareGsdTools(line) {
return false;
}
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
describe('bug #1041: agent files must not call bare gsd-tools (all-runtime resolver)', () => {
test('no agents/gsd-*.md file contains a bare gsd-tools command', () => {
const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
assert.ok(files.length > 0, 'expected agent files to exist');
const violations = [];
for (const f of files) {
const full = path.join(AGENTS_DIR, f);
const content = fs.readFileSync(full, 'utf-8');
const blocks = extractShellBlocks(content);
for (const blk of blocks) {
for (let i = 0; i < blk.lines.length; i++) {
if (lineHasBareGsdTools(blk.lines[i])) {
violations.push(`${f}:${blk.startLine + i}: ${blk.lines[i].trim()}`);
}
}
}
}
assert.deepStrictEqual(
violations,
[],
'Bare `gsd-tools` invocations found in agent shell blocks. ' +
'Inject the runtime-launcher preamble (_runtime-launcher.snippet.sh) and use `gsd_run` instead.\n' +
violations.join('\n'),
);
});
});
describe('bug-2851: workflow files must not call bare `gsd-tools` (#2245 sweep regression)', () => {
test('no gsd-core/workflows/*.md file contains a bare gsd-tools command', () => {
const files = fs.readdirSync(WORKFLOWS_DIR).filter((f) => f.endsWith('.md'));

View File

@@ -32,6 +32,7 @@ const { execFileSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
/**
@@ -115,6 +116,26 @@ function collectWorkflowFiles() {
return results;
}
/**
* Collect all agent .md files under AGENTS_DIR (non-recursive — agents/ has no subdirs,
* but collectFiles in the sync script is recursive-safe; we mirror that here).
*/
function collectAgentFiles() {
const results = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
results.push(full);
}
}
}
walk(AGENTS_DIR);
return results;
}
describe('runtime-launcher-parity (#373)', () => {
// ─── (A) No retired GSD_SDK token ────────────────────────────────────────
test('(A) no GSD_SDK token in any workflow .md file', () => {
@@ -524,3 +545,70 @@ describe('runtime-launcher-parity (#373)', () => {
);
});
});
// ─── Agent parity — runtime-launcher-parity — agents (#1041) ─────────────────
describe('runtime-launcher-parity — agents (#1041)', () => {
// ─── (B-agents) Exactly ONE canonical preamble per using agent file ────────
test('(B-agents) each agent .md using gsd_run contains exactly ONE canonical preamble, before the first gsd_run call', () => {
const preamble = expectedPreamble();
const preambleStr = preamble.join('\n');
const files = collectAgentFiles();
assert.ok(files.length > 0, 'expected at least one agent .md file');
const violations = [];
for (const f of files) {
const rel = path.relative(AGENTS_DIR, f);
const content = fs.readFileSync(f, 'utf8');
const blocks = extractShellBlocks(content);
// Collect all block lines in document order for flat analysis
const allBlockLines = [];
for (const blk of blocks) {
allBlockLines.push(...blk.lines);
}
// Does this file use gsd_run at all?
const fileHasGsdRun = allBlockLines.some((l) => /\bgsd_run\b/.test(l));
if (!fileHasGsdRun) continue; // agents without gsd_run are not checked
// Count preamble occurrences across all shell content of this file
const allContent = allBlockLines.join('\n');
let preambleCount = 0;
let searchPos = 0;
while (true) {
const idx = allContent.indexOf(preambleStr, searchPos);
if (idx === -1) break;
preambleCount++;
searchPos = idx + preambleStr.length;
}
if (preambleCount !== 1) {
violations.push(
`${rel}: expected exactly 1 canonical preamble occurrence in bash blocks, found ${preambleCount}. ` +
`Run \`node scripts/sync-runtime-launcher.cjs\` to fix.`,
);
continue;
}
// Verify preamble appears BEFORE the first gsd_run call (in document order)
const preamblePos = allContent.indexOf(preambleStr);
const firstGsdRunPos = allContent.search(/\bgsd_run\b/);
// The first gsd_run WITHIN the preamble itself (the function definition) is fine.
// Simple check: preamble starts at or before the first gsd_run occurrence.
if (preamblePos > firstGsdRunPos) {
violations.push(
`${rel}: preamble appears AFTER the first gsd_run reference — it must precede all gsd_run calls.`,
);
}
}
assert.deepStrictEqual(
violations,
[],
'Agent files with gsd_run calls have wrong preamble count or ordering:\n' +
violations.join('\n---\n'),
);
});
});