* 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:
@@ -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'));
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user