fix(#2284): project Hermes named dispatch onto delegate_task; protect comparison tables (#2309)

Hermes installs brand-swapped "Claude Code" -> "Hermes Agent" in shipped
workflows/*.md but never projected the Agent(...) dispatch calls onto
Hermes's delegate_task contract, so installed workflows kept literal
Agent(...) syntax and falsely asserted "The Agent tool IS available"
(Hermes exposes delegate_task, not Agent).

Dispatch projection: a generic named-dispatch engine
(projectNamedDispatchToStructuralDelegate) wired into the per-runtime
RUNTIME_CONTENT_DISPATCH.hermes.md converter, branching entirely on the
documentation-sourced hostIntegration.dispatch facts read via
_hostIntegrationDispatch (capability.json unchanged): namedDispatch:false
-> resolve the gsd-* role and embed a load-its-prompt instruction in the
payload; background:true -> map onto delegate_task background; read-only /
maxDepth:1 -> no nested delegation to leaf roles; per-call model dropped.
Span detection uses literal Agent( scanning + local balanced paren/quote
matching (immune to upstream document quote imbalance) and handles all
three corpus call forms (multi-line, object-literal, single-line compact).
An independent, mask-free post-projection guard fails the install loud on
any residual Agent(/subagent_type/leaked model. Fail-closed: install
throws if a literal gsd-* role reference cannot be resolved. commands ->
skill path untouched. No literal Agent( survives in installed Hermes
workflows.

Folded in (maintainer-directed) a pre-existing cross-cutting branding
defect: the "Claude Code" -> brand swap corrupted <runtime_compatibility>
comparison tables (where "Claude Code" is a compared-runtime label) for
every branding runtime. New shared applyClaudeCodeBrandSwap helper
protects <runtime_compatibility> regions via split-and-rejoin (no sentinel
token) while still rebranding genuine self-references; adopted by all six
branding .md converters. Also a surgical prose-consistency fix so
plan-review-convergence.md's dispatch-adjacent terminology is coherent
post-projection (no broad bare-word rename).

Golden install-parity regenerated for the six branding runtimes
(dispatch/branding scope only); other runtimes unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-15 17:10:06 -04:00
committed by GitHub
parent 612fcb00f7
commit 75bedf16fd
11 changed files with 1891 additions and 67 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2309
---
**Runtime brand-swap no longer mislabels `<runtime_compatibility>` comparison tables** — every runtime installer that rebrands "Claude Code" to its own name (Cursor, Windsurf, Trae, Cline, CodeBuddy, Qwen, Hermes) also swapped it inside the runtime-comparison tables in shipped workflows, where "Claude Code" is a compared-runtime label, not a host self-reference — corrupting the comparison. Branding now protects `<runtime_compatibility>` regions while still rebranding genuine self-references. (#2284)

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2309
---
**Hermes installs now project named-agent dispatch onto `delegate_task` instead of asserting a nonexistent `Agent` tool** — installed Hermes workflows brand-swapped "Claude Code"→"Hermes Agent" but kept literal `Agent(...)` calls and falsely claimed "The Agent tool IS available", which Hermes doesn't expose. A Hermes `.md` converter now rewrites named dispatch onto Hermes's `delegate_task` contract (embedding the resolved role prompt since Hermes has no named-agent lookup, mapping background dispatch, dropping unsupported per-call model), driven by the runtime's documented dispatch facts, and fails closed if a referenced role prompt is missing. (#2284)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,895 @@
// allow-test-rule: source-text-is-the-product — see #2284
// Reads installed .md workflow files whose deployed text IS the contract the
// Hermes host reads at runtime — testing text content tests the deployed
// contract, exactly like the sibling hermes-skills-migration test.
/**
* #2284 — Hermes named-dispatch → delegate_task projection.
*
* The Hermes installer previously only brand-swapped "Claude Code" →
* "Hermes Agent" in shipped `gsd-core/workflows/*.md`, leaving a false
* "The Agent tool IS available" assertion and literal `Agent(...)` call
* syntax installed verbatim — Hermes exposes `delegate_task`, not `Agent`.
*
* Covers:
* 1. Direct converter contract — projectNamedDispatchToStructuralDelegate /
* convertClaudeToHermesMarkdown against representative fixture prose,
* across ALL THREE real corpus call-argument shapes: multi-line
* one-key-per-line, single-line object-literal (`Agent({...})` —
* import.md/ingest-docs.md), and single-line compact
* (`Agent(subagent_type="x", model="y", prompt="...")` —
* code-review-fix.md/ship.md/etc).
* 2. Real disposable-HOME `--hermes --global` e2e install — no literal
* `Agent(` survives, `delegate_task` is present, commands→skill path
* (convertClaudeCommandToClaudeSkill) still works unregressed; spot-
* checks import.md, ingest-docs.md, and code-review-fix.md specifically
* (the object-literal and single-line-compact sites).
* 3. Fail-closed role resolution — a referenced gsd-* role prompt missing
* from the shipped agents/ directory aborts install with an explicit
* error, in EVERY call-argument shape, both at the converter level and
* through the real install path (deterministic fs.readdirSync
* injection per the repo's cross-platform IO-failure-injection
* convention — never chmod/permission tricks).
* 4. The post-projection guard (belt-and-suspenders) — fails loud on any
* residual subagent_type / leaked model= / unprojected Agent( the
* projection above did not anticipate, rather than silently shipping it.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const {
convertClaudeToHermesMarkdown,
projectNamedDispatchToStructuralDelegate,
_hostIntegrationDispatch,
_resolveAvailableGsdRoles,
HERMES_DISPATCH_TOOL_CONFIG,
maskStringLiterals,
findDispatchCallSpans,
_assertProjectionComplete,
applyClaudeCodeBrandSwap,
convertClaudeToWindsurfMarkdown,
install,
uninstall,
} = require('../bin/install.js');
const { cleanup } = require('./helpers.cjs');
const { nestedSkillPath } = require('./helpers/nested-layout.cjs');
const HERMES_DISPATCH = _hostIntegrationDispatch('hermes');
// Representative fixture prose mirroring the real shape found in
// gsd-core/workflows/plan-phase.md — the "Agent tool IS available" contract
// assertion followed by a literal, multi-arg Agent(...) dispatch call whose
// subagent_type resolves to a real shipped role.
const FIXTURE_ASSERTION_AND_CALL = [
'The Agent tool IS available in a top-level Hermes Agent session. Always spawn',
'gsd-phase-researcher, gsd-planner, and gsd-plan-checker as separate Agent() calls.',
'',
'```',
'Agent(',
' prompt=filled_research_hook_fragment,',
' subagent_type="gsd-planner",',
' model="{researcher_model}",',
' description="Research Phase {phase}"',
')',
'```',
'',
'> **ORCHESTRATOR RULE — ALL RUNTIMES**: After calling Agent() above, stop working on this task immediately.',
'Wait for the subagent to return its result. Only resume when the subagent result is available.',
].join('\n');
function hermesToolConfig(overrides = {}) {
return Object.assign({}, HERMES_DISPATCH_TOOL_CONFIG, {
availableRoles: _resolveAvailableGsdRoles(),
runtime: 'hermes',
}, overrides);
}
// ─── 1. Direct converter contract ────────────────────────────────────────────
describe('#2284 convertClaudeToHermesMarkdown / projectNamedDispatchToStructuralDelegate — converter contract', () => {
test('capabilities/hermes/capability.json dispatch facts are unchanged (docs-sourced, not touched by this fix)', () => {
// Locks in the maintainer-confirmed constraint: this fix reads the
// existing sourced facts, it never edits them.
assert.strictEqual(HERMES_DISPATCH.namedDispatch, false);
assert.strictEqual(HERMES_DISPATCH.background, true);
assert.strictEqual(HERMES_DISPATCH.backgroundDispatch, false);
assert.strictEqual(HERMES_DISPATCH.subagentToolkit, 'read-only');
assert.strictEqual(HERMES_DISPATCH.maxDepth, 1);
assert.strictEqual(HERMES_DISPATCH.nested, true);
});
test('no literal Agent( call syntax survives the projection', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(!/\bAgent\(/.test(out), `literal Agent( survived:\n${out}`);
});
test('emits a delegate_task-shaped dispatch call with the resolved role reference', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(/delegate_task\(/.test(out), 'delegate_task( call syntax present');
assert.ok(/gsd_role="gsd-planner"/.test(out), 'gsd_role carries the resolved role identifier');
assert.ok(/gsd_role_prompt=/.test(out), 'gsd_role_prompt carries the loaded-content instruction');
assert.ok(/role="leaf"/.test(out), 'structural role pinned to Hermes\'s non-orchestrating leaf value');
});
test('drops per-call model forwarding (host-model inheritance is explicit)', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(!/model="\{researcher_model\}"/.test(out), 'per-call model="{researcher_model}" line stripped');
assert.ok(!/\bmodel=/.test(out), 'no model= parameter forwarded anywhere in the projected call');
});
test('the "Agent tool IS available" assertion becomes an accurate delegate_task statement', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(!/Agent tool IS available/.test(out), 'false Claude-shaped assertion removed');
assert.ok(/delegate_task/.test(out), 'assertion references the real Hermes dispatch primitive');
assert.ok(/no concept of a named subagent identity/i.test(out), 'assertion states the roleless-lookup contract (namedDispatch: false)');
});
test('async halt/resume wording is preserved (no busy-poll)', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(/stop working on this task immediately/.test(out), 'halt-after-dispatch instruction preserved');
assert.ok(/Wait for the subagent to return its result/.test(out), 'resume-on-completion instruction preserved');
});
test('fail-closed wording is present for the role-resolution step', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE_ASSERTION_AND_CALL, { runtime: 'hermes' });
assert.ok(/FAIL CLOSED/.test(out), 'explicit FAIL CLOSED instruction present');
assert.ok(/never execute the role inline/i.test(out), 'explicit prohibition on silent inline execution');
});
test('run_in_background= maps onto Hermes\'s native background= (dispatch.background: true)', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-executor",\n run_in_background=true,\n description="d"\n)';
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(/\bbackground=true\b/.test(out), 'background=true present');
assert.ok(!/run_in_background=/.test(out), 'Claude-native run_in_background= param name gone');
});
test('genuinely branches on dispatch.background: false — strips (never renames) the background flag', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-executor",\n run_in_background=true,\n description="d"\n)';
const out = projectNamedDispatchToStructuralDelegate(
fixture,
Object.assign({}, HERMES_DISPATCH, { background: false }),
hermesToolConfig(),
);
assert.ok(!/run_in_background=/.test(out), 'unsupported flag not left in Claude form');
assert.ok(!/\bbackground=true\b/.test(out), 'flag not forwarded when dispatch.background is false');
});
test('genuinely branches on dispatch.namedDispatch: true — passes named dispatch through unprojected', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-executor",\n description="d"\n)';
const out = projectNamedDispatchToStructuralDelegate(
fixture,
Object.assign({}, HERMES_DISPATCH, { namedDispatch: true }),
hermesToolConfig(),
);
// No role-prompt-embedding machinery should be injected when the target
// primitive can resolve named agents itself.
assert.ok(!/gsd_role_prompt=/.test(out), 'no prompt-content-embedding injected when namedDispatch is true');
assert.ok(!/FAIL CLOSED/.test(out), 'no fail-closed role-resolution injected when namedDispatch is true');
assert.ok(/delegate_task\(/.test(out), 'call syntax still renamed to the target tool name');
});
test('genuinely branches on subagentToolkit/maxDepth — omits the depth/toolkit caveat when the target can orchestrate', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-executor",\n description="d"\n)';
const restrictedOut = projectNamedDispatchToStructuralDelegate(
fixture, HERMES_DISPATCH, hermesToolConfig(),
);
assert.ok(/nested delegation is unavailable/.test(restrictedOut), 'read-only/depth-1 caveat present for the real sourced facts');
const orchestrateCapableOut = projectNamedDispatchToStructuralDelegate(
fixture,
Object.assign({}, HERMES_DISPATCH, { subagentToolkit: 'full', maxDepth: -1 }),
hermesToolConfig(),
);
assert.ok(!/nested delegation is unavailable/.test(orchestrateCapableOut), 'caveat omitted when the target genuinely supports nested delegation');
});
test('preserves body content and prose the projection does not target', () => {
const fixture = 'Some unrelated prose.\n\nAgent(\n prompt=x,\n subagent_type="gsd-verifier",\n description="d"\n)\n\nMore unrelated prose.';
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(out.includes('Some unrelated prose.'));
assert.ok(out.includes('More unrelated prose.'));
});
});
// ─── 1b. All three real corpus call-argument shapes ─────────────────────────
describe('#2284 all three real corpus Agent(...) call-argument shapes', () => {
// (a) multi-line, one key= per line — plan-phase.md/execute-phase.md/etc.
const MULTI_LINE = 'Agent(\n prompt=x,\n subagent_type="gsd-planner",\n model="{researcher_model}",\n description="d"\n)';
// (b) single-line object-literal (colon syntax) — import.md/ingest-docs.md.
const OBJECT_LITERAL = 'Agent({\n subagent_type: "gsd-plan-checker",\n prompt: "Validate the plan."\n})';
// (c) single-line compact — code-review-fix.md/code-review.md/ship.md/etc.
const SINGLE_LINE_COMPACT = 'Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="Fix the findings.")';
const forms = [
['multi-line one-key-per-line', MULTI_LINE, 'gsd-planner'],
['single-line object-literal', OBJECT_LITERAL, 'gsd-plan-checker'],
['single-line compact', SINGLE_LINE_COMPACT, 'gsd-code-fixer'],
];
for (const [label, fixture, role] of forms) {
test(`${label}: projects to delegate_task with gsd_role_prompt + role="leaf"`, () => {
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(/delegate_task\(/.test(out), `${label}: delegate_task( present`);
assert.ok(out.includes(`gsd_role="${role}"`), `${label}: gsd_role carries "${role}"`);
assert.ok(/gsd_role_prompt=/.test(out), `${label}: gsd_role_prompt injected`);
assert.ok(/role="leaf"/.test(out), `${label}: structural role="leaf" injected`);
assert.ok(/FAIL CLOSED/.test(out), `${label}: fail-closed wording present`);
});
test(`${label}: no residual subagent_type (either = or : syntax)`, () => {
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(!/\bsubagent_type\s*[=:]/.test(out), `${label}: subagent_type token gone:\n${out}`);
});
test(`${label}: no leaked model= (host-model inheritance, never forwarded)`, () => {
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
const mask = maskStringLiterals(out);
assert.ok(!/\bmodel\s*[=:]/.test(mask), `${label}: no model= or model: token survives:\n${out}`);
});
test(`${label}: a bogus role triggers the fail-closed throw`, () => {
const bogusFixture = fixture.replace(role, 'gsd-totally-fake-role-2284');
assert.throws(
() => convertClaudeToHermesMarkdown(bogusFixture, { runtime: 'hermes' }),
/gsd-totally-fake-role-2284/,
`${label}: expected an explicit fail-closed error naming the bogus role`,
);
});
}
test('object-literal wrapper braces are stripped (Hermes delegate_task is a flat kwarg call)', () => {
const out = convertClaudeToHermesMarkdown(OBJECT_LITERAL, { runtime: 'hermes' });
assert.ok(!/delegate_task\(\s*\{/.test(out), 'no leftover "{" immediately after delegate_task(');
assert.ok(!/\}\s*\)\s*$/.test(out.trim()), 'no leftover "}" immediately before the closing )');
});
test('object-literal form: non-role/model keys (e.g. prompt:) are left in their original colon style', () => {
const out = convertClaudeToHermesMarkdown(OBJECT_LITERAL, { runtime: 'hermes' });
assert.ok(out.includes('prompt: "Validate the plan."'), 'untouched arg keys keep their original syntax');
});
test('single-line-compact: a real corpus fixture identical to code-review-fix.md:201 shape (multi-line prompt body opened on the compact head)', () => {
// code-review-fix.md's real shape: `Agent(subagent_type="x", model="y", prompt="` opens a
// MULTI-LINE prompt body (no escaping) that closes many lines later with `")`.
const fixture = [
'Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="',
'<files_to_read>',
'${REVIEW_PATH}',
'</files_to_read>',
'',
'Read REVIEW.md findings, apply fixes.',
'${AGENT_SKILLS_FIXER}")',
].join('\n');
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(!/\bAgent\(/.test(out), 'no literal Agent( survives a multi-line-body compact-head call');
assert.ok(/delegate_task\(/.test(out));
assert.ok(out.includes('gsd_role="gsd-code-fixer"'));
assert.ok(!/\bmodel\s*[=:]/.test(maskStringLiterals(out)), 'model stripped even though the prompt body spans many lines');
assert.ok(out.includes('<files_to_read>'), 'multi-line prompt BODY content is preserved verbatim');
assert.ok(out.includes('${REVIEW_PATH}'), 'interpolation placeholders inside the prompt body are untouched');
});
test('disconnected prose mention (not part of any real Agent(...) call, e.g. map-codebase.md-style) is still renamed and validated', () => {
const fixture = 'Use Agent tool with `subagent_type="gsd-codebase-mapper"` for parallel execution.';
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(out.includes('gsd_role="gsd-codebase-mapper"'), 'prose mention renamed to gsd_role=');
assert.ok(!/\bsubagent_type\s*[=:]/.test(out));
});
test('a documentation TEMPLATE placeholder role (curly-brace interpolation, e.g. universal-anti-patterns.md\'s subagent_type: "gsd-{agent}") is renamed but NOT fail-closed validated', () => {
const fixture = 'ALWAYS use `subagent_type: "gsd-{agent}"` (e.g., `gsd-phase-researcher`, `gsd-executor`).';
assert.doesNotThrow(() => convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' }));
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(out.includes('gsd_role: "gsd-{agent}"'), 'template placeholder renamed, value preserved verbatim');
});
test('run_in_background: true (colon-prose form, e.g. execute-phase.md) maps onto background: true, same as the = form', () => {
const fixture = 'Dispatch each `Agent()` call one at a time with `run_in_background: true`.';
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(/background:\s*true/.test(out), 'colon-prose form mapped to the native background param');
assert.ok(!/run_in_background/.test(out), 'Claude-native run_in_background token gone');
});
test('a call site preceded by explanatory comments describing the now-removed model= conditional strips both the arg AND the dead comments (Finding 5)', () => {
const fixture = [
'Agent(',
' subagent_type="gsd-executor",',
' description="Execute plan",',
' # Only include model= when executor_model is an explicit model name.',
' # When executor_model is "inherit", omit this parameter entirely so',
' # Claude Code inherits the orchestrator model automatically.',
' model="{executor_model}", # omit this line when executor_model == "inherit"',
' isolation="worktree",',
' prompt="Execute the plan."',
')',
].join('\n');
const out = convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' });
assert.ok(!/model="\{executor_model\}"/.test(out), 'model= argument line removed');
assert.ok(!/Only include model=/.test(out), 'dead explanatory comment (line 1) removed');
assert.ok(!/omit this parameter entirely/.test(out), 'dead explanatory comment (line 2) removed');
assert.ok(!/inherits the orchestrator model/.test(out), 'dead explanatory comment (line 3) removed');
assert.ok(out.includes('isolation="worktree"'), 'unrelated surrounding arguments preserved');
});
});
// ─── 1c. Post-projection guard (belt-and-suspenders, #2284 requirement 3) ───
describe('#2284 post-projection guard — fails loud on any unanticipated residual form', () => {
const toolConfig = hermesToolConfig();
test('throws when a residual subagent_type token survives (any syntax)', () => {
assert.throws(
() => _assertProjectionComplete('delegate_task(subagent_type="gsd-planner")', toolConfig),
/residual subagent_type/i,
);
assert.throws(
() => _assertProjectionComplete('delegate_task(subagent_type: "gsd-planner")', toolConfig),
/residual subagent_type/i,
);
});
test('throws when literal Agent( call syntax survives', () => {
// Isolated from the subagent_type check above (which fires first and
// would otherwise mask this assertion) — a bare Agent() mention with no
// remaining subagent_type token.
assert.throws(
() => _assertProjectionComplete('Please call Agent() to dispatch.', toolConfig),
/literal Agent\(/i,
);
});
test('throws when a model= argument leaks inside a delegate_task(...) call', () => {
assert.throws(
() => _assertProjectionComplete('delegate_task(gsd_role="gsd-planner", model="{m}")', toolConfig),
/leaked model=/i,
);
});
test('does NOT throw on a clean, fully-projected document', () => {
const clean = 'delegate_task(gsd_role="gsd-planner", gsd_role_prompt=<resolve...>, role="leaf", prompt="x")';
assert.doesNotThrow(() => _assertProjectionComplete(clean, toolConfig));
});
test('does NOT flag Agent( or subagent_type mentioned INSIDE a quoted string (not real call syntax)', () => {
// e.g. settings.md: `description: "Chain stages via Agent() subagents"`.
const proseInsideString = 'delegate_task(description="Chain stages via Agent() subagents, not subagent_type=x")';
assert.doesNotThrow(() => _assertProjectionComplete(proseInsideString, toolConfig));
});
test('findDispatchCallSpans correctly balances parens across a quoted prompt body containing its own parens', () => {
// Mirrors discuss-phase-assumptions.md's real shape: parenthetical prose
// ("(e.g., ...)") embedded inside a triple-quoted prompt body.
const fixture = 'Agent(subagent_type="gsd-verifier", prompt="""\nAnalyze (e.g., "Technical Approach") the codebase.\n(3-5 areas, calibrated by tier)\n""")';
const spans = findDispatchCallSpans(fixture, 'Agent');
assert.strictEqual(spans.length, 1, 'exactly one call span found despite embedded parens');
assert.strictEqual(spans[0].end, fixture.length, 'span correctly extends to the TRUE closing paren, not a premature one inside the string');
});
});
// ─── 2. Real disposable-HOME e2e install ─────────────────────────────────────
describe('#2284 real disposable-HOME --hermes --global install', () => {
let tmpHome;
let savedHome;
let savedUserProfile;
let savedHermesHome;
beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2284-hermes-home-'));
savedHome = process.env.HOME;
savedUserProfile = process.env.USERPROFILE;
savedHermesHome = process.env.HERMES_HOME;
process.env.HOME = tmpHome;
process.env.USERPROFILE = tmpHome;
process.env.HERMES_HOME = path.join(tmpHome, '.hermes');
});
afterEach(() => {
try {
uninstall(true, 'hermes');
} catch (_e) {
// best-effort — some fail-closed tests intentionally leave a partial install
}
if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome;
if (savedUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = savedUserProfile;
if (savedHermesHome === undefined) delete process.env.HERMES_HOME; else process.env.HERMES_HOME = savedHermesHome;
cleanup(tmpHome);
});
test('installed plan-phase.md has no literal Agent( and does contain delegate_task', () => {
const result = install(true, 'hermes');
assert.strictEqual(result.runtime, 'hermes');
const planPhasePath = path.join(result.configDir, 'gsd-core', 'workflows', 'plan-phase.md');
assert.ok(fs.existsSync(planPhasePath), `expected installed workflow at ${planPhasePath}`);
const content = fs.readFileSync(planPhasePath, 'utf8');
assert.ok(!/\bAgent\(/.test(content), 'no literal Agent( call syntax in installed plan-phase.md');
assert.ok(/delegate_task\(/.test(content), 'delegate_task( present in installed plan-phase.md');
assert.ok(!/Agent tool IS available/.test(content), 'false assertion not installed verbatim');
});
test('spot-check a second workflow (execute-phase.md) — same guarantees hold', () => {
const result = install(true, 'hermes');
const executePhasePath = path.join(result.configDir, 'gsd-core', 'workflows', 'execute-phase.md');
assert.ok(fs.existsSync(executePhasePath));
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(!/\bAgent\(/.test(content), 'no literal Agent( call syntax in installed execute-phase.md');
assert.ok(/delegate_task\(/.test(content), 'delegate_task( present in installed execute-phase.md');
});
test('spot-check import.md (object-literal Agent({...}) form) in the installed tree', () => {
const result = install(true, 'hermes');
const importPath = path.join(result.configDir, 'gsd-core', 'workflows', 'import.md');
assert.ok(fs.existsSync(importPath));
const content = fs.readFileSync(importPath, 'utf8');
assert.ok(!/\bAgent\(/.test(content), 'no literal Agent( in installed import.md');
assert.ok(!/\bsubagent_type\s*[=:]/.test(content), 'no residual subagent_type in installed import.md');
assert.ok(content.includes('gsd_role="gsd-plan-checker"'), 'gsd_role carries the resolved role');
assert.ok(/gsd_role_prompt=/.test(content), 'role-prompt-resolution injected');
});
test('spot-check ingest-docs.md (object-literal Agent({...}) form, two call sites) in the installed tree', () => {
const result = install(true, 'hermes');
const ingestPath = path.join(result.configDir, 'gsd-core', 'workflows', 'ingest-docs.md');
assert.ok(fs.existsSync(ingestPath));
const content = fs.readFileSync(ingestPath, 'utf8');
assert.ok(!/\bAgent\(/.test(content), 'no literal Agent( in installed ingest-docs.md');
assert.ok(!/\bsubagent_type\s*[=:]/.test(content), 'no residual subagent_type in installed ingest-docs.md');
assert.ok(content.includes('gsd_role="gsd-doc-synthesizer"'), 'first call site role resolved');
assert.ok(content.includes('gsd_role="gsd-roadmapper"'), 'second call site role resolved');
});
test('spot-check code-review-fix.md (single-line-compact Agent(subagent_type=..., model=..., prompt="multi-line body) form) in the installed tree', () => {
const result = install(true, 'hermes');
const crfPath = path.join(result.configDir, 'gsd-core', 'workflows', 'code-review-fix.md');
assert.ok(fs.existsSync(crfPath));
const content = fs.readFileSync(crfPath, 'utf8');
assert.ok(!/\bAgent\(/.test(content), 'no literal Agent( in installed code-review-fix.md');
assert.ok(!/\bsubagent_type\s*[=:]/.test(content), 'no residual subagent_type in installed code-review-fix.md');
const mask = maskStringLiterals(content);
assert.ok(!/\bmodel\s*[=:]/.test(mask), 'no leaked model= inside any real call in installed code-review-fix.md');
assert.ok(content.includes('gsd_role="gsd-code-fixer"'), 'gsd-code-fixer role resolved');
assert.ok(content.includes('gsd_role="gsd-code-reviewer"'), 'gsd-code-reviewer role resolved (2nd/3rd call sites)');
});
test('EVERY installed workflow file is free of literal Agent( call syntax', () => {
const result = install(true, 'hermes');
const workflowsDir = path.join(result.configDir, 'gsd-core', 'workflows');
assert.ok(fs.existsSync(workflowsDir));
const files = fs.readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
assert.ok(files.length > 10, 'sanity: a real corpus of workflow files was installed');
for (const f of files) {
const content = fs.readFileSync(path.join(workflowsDir, f), 'utf8');
assert.ok(!/\bAgent\(/.test(content), `${f} still contains literal Agent( call syntax`);
}
});
test('commands/gsd/*.md → Hermes-skill path (convertClaudeCommandToClaudeSkill) still works, unregressed', () => {
const result = install(true, 'hermes');
const categoryDir = path.join(result.configDir, 'skills', 'gsd');
assert.ok(fs.existsSync(categoryDir), 'skills/gsd category dir installed');
const helpSkillPath = nestedSkillPath(categoryDir, 'gsd-', 'help');
assert.ok(fs.existsSync(helpSkillPath), `expected nested skill at ${helpSkillPath}`);
const skillContent = fs.readFileSync(helpSkillPath, 'utf8');
assert.ok(/^---/.test(skillContent), 'skill file has YAML frontmatter');
assert.ok(/name:\s*gsd-help/.test(skillContent), 'skill frontmatter name is the canonical gsd-help');
});
});
// ─── 3. Fail-closed role resolution ──────────────────────────────────────────
describe('#2284 fail-closed role resolution', () => {
test('converter throws when a literal gsd_role reference has no matching shipped role', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-totally-fake-role-2284",\n description="d"\n)';
assert.throws(
() => convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' }),
/gsd-totally-fake-role-2284/,
'expected an explicit error naming the unresolvable role',
);
});
test('converter throws (never silently installs) when the agents/ directory cannot be resolved at all', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-planner",\n description="d"\n)';
assert.throws(
() => projectNamedDispatchToStructuralDelegate(fixture, HERMES_DISPATCH, hermesToolConfig({ availableRoles: null })),
/could not resolve/i,
);
});
test('a literal reference to a role that DOES exist never throws', () => {
const fixture = 'Agent(\n prompt=x,\n subagent_type="gsd-verifier",\n description="d"\n)';
assert.doesNotThrow(() => convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' }));
});
test('dynamic (non-literal) role references are not statically checked and never throw', () => {
// Mirrors the real plan-phase.md shape: subagent_type=research_hook.ref.agent
// is resolved at runtime by the host, not a literal string install.js can verify.
const fixture = 'Agent(\n prompt=x,\n subagent_type=research_hook.ref.agent,\n description="d"\n)';
assert.doesNotThrow(() => convertClaudeToHermesMarkdown(fixture, { runtime: 'hermes' }));
});
describe('real install path — deterministic fs.readdirSync injection (never chmod/permission tricks)', () => {
let tmpHome;
let savedHome;
let savedUserProfile;
let savedHermesHome;
let origReaddirSync;
let injectedAgentsDir;
beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2284-hermes-failclosed-'));
savedHome = process.env.HOME;
savedUserProfile = process.env.USERPROFILE;
savedHermesHome = process.env.HERMES_HOME;
process.env.HOME = tmpHome;
process.env.USERPROFILE = tmpHome;
process.env.HERMES_HOME = path.join(tmpHome, '.hermes');
injectedAgentsDir = path.resolve(__dirname, '..', 'agents');
origReaddirSync = fs.readdirSync;
});
afterEach(() => {
fs.readdirSync = origReaddirSync;
try {
uninstall(true, 'hermes');
} catch (_e) {
// best-effort — the install intentionally failed partway through
}
if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome;
if (savedUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = savedUserProfile;
if (savedHermesHome === undefined) delete process.env.HERMES_HOME; else process.env.HERMES_HOME = savedHermesHome;
cleanup(tmpHome);
});
test('a real --hermes --global install aborts with an explicit error when the shipped agents/ dir is unreadable', () => {
fs.readdirSync = function (p, opts) {
if (typeof p === 'string' && path.resolve(p) === injectedAgentsDir) {
throw new Error('#2284 injected fs.readdirSync failure — simulated unreadable agents/ dir');
}
return origReaddirSync.call(fs, p, opts);
};
assert.throws(
() => install(true, 'hermes'),
/could not resolve|refusing to install/i,
'a real hermes install must fail closed, never silently install workflows with unverifiable role references',
);
});
});
});
// ─── 5. Corpus-wide invariant (round-2 CRITICAL regression guard) ───────────
//
// #2284 round-2: `findDispatchCallSpans` originally relied on WHOLE-DOCUMENT
// cumulative quote parity (`maskStringLiterals` run once over the entire
// file). A markdown workflow mixes prose, ```bash fences full of their own
// double-quoted strings, and shell quoting — there is no single document-wide
// quote grammar. In the real corpus, a `"`-heavy bash block upstream of
// gsd-core/workflows/code-review.md's real
// `Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", ...)`
// call (~line 488) desynced that cumulative state, making the span detector
// blind to the call. It shipped completely unnormalized except for the
// catch-all's `subagent_type=`→`gsd_role=` rename: a Frankenstein
// `Agent(gsd_role="gsd-code-reviewer", model="{REVIEWER_MODEL}", ...)` — head
// still literal `Agent(`, `model=` leaked, no `gsd_role_prompt`/`role="leaf"`
// injected. A per-file/spot-check test suite did not exercise this file's
// exact shape and missed it; THIS is the real regression protection —
// hash-only goldens cannot catch a semantic defect like this.
describe('#2284 corpus-wide invariant — every shipped workflow/reference/template .md', () => {
function walkMarkdown(dir) {
if (!fs.existsSync(dir)) return [];
let out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out = out.concat(walkMarkdown(full));
else if (entry.name.endsWith('.md')) out.push(full);
}
return out;
}
const CORPUS_ROOT = path.join(__dirname, '..', 'gsd-core');
const CORPUS_FILES = ['workflows', 'references', 'templates', 'contexts']
.flatMap((sub) => walkMarkdown(path.join(CORPUS_ROOT, sub)));
test('sanity: a real, substantial corpus was found to scan', () => {
assert.ok(CORPUS_FILES.length > 100, `expected >100 shipped .md files, found ${CORPUS_FILES.length}`);
});
test('every shipped .md file projects with ZERO residual Agent(, ZERO residual subagent_type, and ZERO leaked model= inside any delegate_task(...) call', () => {
const failures = [];
let totalDelegateTaskCalls = 0;
for (const file of CORPUS_FILES) {
const rel = path.relative(CORPUS_ROOT, file);
const content = fs.readFileSync(file, 'utf8');
let out;
try {
out = convertClaudeToHermesMarkdown(content, { runtime: 'hermes' });
} catch (e) {
failures.push(`${rel}: converter threw unexpectedly: ${e.message}`);
continue;
}
if (/\bAgent\(/.test(out)) failures.push(`${rel}: residual literal Agent( survives`);
if (/\bsubagent_type\s*[=:]/.test(out)) failures.push(`${rel}: residual subagent_type survives`);
for (const span of findDispatchCallSpans(out, 'delegate_task')) {
const rawSpanText = out.slice(span.start, span.end);
if (/\bmodel\s*[=:]/.test(rawSpanText)) failures.push(`${rel}: leaked model= inside a delegate_task(...) call`);
}
totalDelegateTaskCalls += (out.match(/delegate_task\(/g) || []).length;
}
assert.deepStrictEqual(failures, [], `corpus-wide invariant violations:\n${failures.join('\n')}`);
assert.ok(totalDelegateTaskCalls > 50, `sanity: expected a substantial number of real delegate_task( calls emitted, got ${totalDelegateTaskCalls}`);
});
test('code-review.md specifically: the real Agent(subagent_type="gsd-code-reviewer", model=..., prompt=...) call (~line 488) projects cleanly despite an upstream `"`-heavy bash fence', () => {
const file = path.join(CORPUS_ROOT, 'workflows', 'code-review.md');
const content = fs.readFileSync(file, 'utf8');
const out = convertClaudeToHermesMarkdown(content, { runtime: 'hermes' });
assert.ok(!/\bAgent\(/.test(out), 'no literal Agent( survives in code-review.md');
assert.ok(!/\bsubagent_type\s*[=:]/.test(out), 'no residual subagent_type in code-review.md');
assert.ok(out.includes('gsd_role="gsd-code-reviewer"'), 'the real call\'s role is resolved, not just the catch-all rename');
const callStart = out.indexOf('delegate_task(gsd_role="gsd-code-reviewer"');
assert.ok(callStart !== -1, 'the real call head IS delegate_task( — not a bare catch-all-renamed Agent( survivor');
const callSpans = findDispatchCallSpans(out, 'delegate_task');
const realCallSpan = callSpans.find((s) => s.start === callStart);
assert.ok(realCallSpan, 'the real call is detected as a complete, well-formed delegate_task(...) span');
const rawCall = out.slice(realCallSpan.start, realCallSpan.end);
assert.ok(/gsd_role_prompt=/.test(rawCall), 'gsd_role_prompt injected into the real call');
assert.ok(/role="leaf"/.test(rawCall), 'role="leaf" injected into the real call');
assert.ok(!/\bmodel\s*[=:]/.test(rawCall), 'no model= leaked inside the real call');
});
});
// ─── 6. Bash-fence quote-imbalance regression (round-2 root cause) ──────────
describe('#2284 bash-fence quote-imbalance before a real call (round-2 root cause)', () => {
// Minimal repro of code-review.md's real shape: a ```bash fence containing
// an ODD/unbalanced count of literal double-quotes (ordinary, realistic
// shell prose — `echo "..."` plus a nested escaped quote), followed by a
// real Agent(...) call further down in the SAME document. Under the
// round-1 whole-document cumulative-quote-parity bug, the fence's
// unbalanced quoting flipped the parser's "am I inside a string" state by
// the time it reached the real call, making the call invisible to
// `findDispatchCallSpans` entirely.
const FIXTURE = [
'```bash',
'echo "Warning: skipping structural findings embed (${SIZE} bytes). Re-run if needed."',
'if [ -n "$X" ]; then echo "note: check the \\"quoted\\" value"; fi',
'```',
'',
'Spawn the reviewer:',
'',
'```',
'Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="',
'<files_to_read>',
'${FILES_TO_READ}',
'</files_to_read>',
'Review and report.',
'")',
'```',
].join('\n');
test('findDispatchCallSpans finds the real call despite the upstream quote-heavy bash fence', () => {
const spans = findDispatchCallSpans(FIXTURE, 'Agent');
assert.strictEqual(spans.length, 1, 'exactly one Agent(...) call span found');
assert.ok(FIXTURE.slice(spans[0].start, spans[0].end).startsWith('Agent(subagent_type="gsd-code-reviewer"'));
});
test('the fixture projects fully and correctly (delegate_task head, role injected, no model leak, no residual Agent()', () => {
const out = convertClaudeToHermesMarkdown(FIXTURE, { runtime: 'hermes' });
assert.ok(!/\bAgent\(/.test(out), 'no literal Agent( survives');
assert.ok(!/\bsubagent_type\s*[=:]/.test(out), 'no residual subagent_type');
assert.ok(out.includes('delegate_task(gsd_role="gsd-code-reviewer"'), 'real call head IS delegate_task(, role resolved inline at the head — not a bare catch-all rename');
assert.ok(/gsd_role_prompt=/.test(out), 'role-prompt-resolution injected');
assert.ok(/role="leaf"/.test(out), 'structural role injected');
const mask = maskStringLiterals(out);
assert.ok(!/\bmodel\s*[=:]/.test(mask), 'no model= leaked');
});
test('the independent post-projection guard catches the deliberately-broken (un-normalized) output this exact fixture used to produce', () => {
// The round-1/round-2 Frankenstein output: catch-all renamed
// subagent_type=→gsd_role= but the head stayed literal Agent( and
// model= leaked through, because the call was never detected as a span.
const frankenstein = 'Agent(gsd_role="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="Review and report.")';
const toolConfig = hermesToolConfig();
assert.throws(
() => _assertProjectionComplete(frankenstein, toolConfig),
/literal Agent\(/i,
'the independent guard must fail loud on the exact Frankenstein shape the bug produced',
);
});
});
// ─── 7. plan-review-convergence.md dispatch-adjacent terminology (LOW finding a) ──
//
// The projection renamed `Agent(`→`delegate_task(` but originally left
// adjacent bare-word "Agent" references in the SAME sentence/paragraph
// un-normalized (gsd-core/workflows/plan-review-convergence.md ~lines 108,
// 347, 355), producing self-contradictory installed Hermes text (e.g.
// "...delegate_task(...)... the convergence orchestrator runs at depth 0
// with Agent available..."). Fixed via two narrowly-scoped exact-phrase
// replacements (NOT a broad bare-word `Agent` rename, which would corrupt
// legitimate `Agent`-adjacent prose elsewhere — role names, "Agent Brief",
// agent-file references).
describe('#2284 plan-review-convergence.md dispatch-adjacent terminology consistency (LOW finding a)', () => {
const FILE = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-review-convergence.md');
const CONTENT = fs.readFileSync(FILE, 'utf8');
const OUT = convertClaudeToHermesMarkdown(CONTENT, { runtime: 'hermes' });
test('sanity: the source file still contains the two flagged dispatch-adjacent phrases (regression canary for this test itself)', () => {
assert.ok(/orchestrator runs at depth 0 with Agent available/.test(CONTENT), 'source phrase 1 present');
assert.ok(/\(bug #936: depth-1 Agent has no Agent tool\)/.test(CONTENT), 'source phrase 2 present');
});
test('no literal Agent( survives and no residual bare "Agent available"/"Agent has no Agent tool" contradiction', () => {
assert.ok(!/\bAgent\(/.test(OUT), 'no literal Agent( call syntax survives');
assert.ok(!/\bAgent available\b/.test(OUT), 'no bare "Agent available" left adjacent to a renamed delegate_task( mention');
assert.ok(!/depth-1 Agent has no/.test(OUT), 'no bare "depth-1 Agent" left adjacent to the renamed delegate_task(');
});
test('both flagged paragraphs (source ~lines 108, 347) consistently say "delegate_task available"', () => {
const matches = OUT.match(/orchestrator runs at depth 0 with delegate_task available/g) || [];
assert.strictEqual(matches.length, 2, 'both paragraphs (initial planning + replan) normalized consistently');
});
test('the success_criteria bullet (source ~line 355) reads consistently: "depth-1 delegate_task has no nested delegate_task"', () => {
assert.ok(OUT.includes('(bug #936: depth-1 delegate_task has no nested delegate_task)'));
});
test('unrelated bare "Agent" mentions NOT adjacent to a renamed dispatch call are left untouched (no broad rename)', () => {
// "Review via Agent → Skill(...)" (success_criteria) has no Agent(...)
// call in the same bullet — the projection never touched it, so it must
// not be renamed either.
assert.ok(OUT.includes('Review via Agent → Skill("gsd-review")'), 'unrelated bare "Agent" prose left intact — no broad bare-word rename');
// "Hermes Agent" is the runtime's own brand name (from brandingRewrites),
// never the dispatch primitive — must never be touched by this fix.
assert.ok(OUT.includes('the one level of nesting that works on Hermes Agent'), 'runtime brand name "Hermes Agent" untouched by the dispatch-terminology fix');
});
});
// ─── 8. Branding protected-region — <runtime_compatibility> tables (finding b) ──
//
// The shared "Claude Code" → host-brand-name swap (applied by EVERY runtime
// that brands workflow content: cursor/windsurf/trae/cline/codebuddy
// hardcoded, qwen/hermes descriptor-driven) rewrote "Claude Code" even
// inside `<runtime_compatibility>` comparison tables
// (gsd-core/workflows/{plan-phase,execute-phase}.md), where "Claude Code" is
// a COMPARED-RUNTIME LABEL, not a host self-reference — mislabeling the
// comparison. Cross-cutting: reproduces on every branding runtime, not just
// Hermes. Fixed via `applyClaudeCodeBrandSwap`, a protected-region
// extract/restore wrapper used by every runtime's brand-swap call site.
describe('#2284(b) branding protected-region — <runtime_compatibility> comparison tables', () => {
test('applyClaudeCodeBrandSwap leaves <runtime_compatibility> content byte-identical, but still swaps self-references outside it', () => {
const fixture = [
'This tool runs on Claude Code and other hosts.',
'',
'<runtime_compatibility>',
'- **Claude Code:** Uses `Agent(...)` — blocks until complete',
'- **Other runtimes:** sequential inline execution',
'</runtime_compatibility>',
'',
'Claude Code users should also read CONTRIBUTING.md.',
].join('\n');
const out = applyClaudeCodeBrandSwap(fixture, 'Windsurf');
const block = out.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(block.includes('**Claude Code:**'), 'compared-runtime label inside the block is untouched');
assert.ok(!block.includes('Windsurf'), 'the block never gains the installing runtime\'s own brand name');
assert.ok(out.includes('This tool runs on Windsurf and other hosts.'), 'genuine self-reference BEFORE the block is branded');
assert.ok(out.includes('Windsurf users should also read CONTRIBUTING.md.'), 'genuine self-reference AFTER the block is branded');
});
test('a no-op brand name (falsy) returns content unchanged (fail-closed default, matches the qwen/hermes "guarded" no-op pattern)', () => {
const fixture = 'Claude Code does the thing.';
assert.strictEqual(applyClaudeCodeBrandSwap(fixture, undefined), fixture);
assert.strictEqual(applyClaudeCodeBrandSwap(fixture, null), fixture);
assert.strictEqual(applyClaudeCodeBrandSwap(fixture, ''), fixture);
});
test('multiple <runtime_compatibility> blocks in the same document are each protected independently', () => {
const fixture = [
'<runtime_compatibility>Claude Code: A</runtime_compatibility>',
'Claude Code self-reference.',
'<runtime_compatibility>Claude Code: B</runtime_compatibility>',
].join('\n');
const out = applyClaudeCodeBrandSwap(fixture, 'Trae');
assert.ok(out.includes('<runtime_compatibility>Claude Code: A</runtime_compatibility>'));
assert.ok(out.includes('<runtime_compatibility>Claude Code: B</runtime_compatibility>'));
assert.ok(out.includes('Trae self-reference.'));
});
test('collision-robust: arbitrary sentinel-like content in surrounding prose (NUL byte, <!--PLACEHOLDER--> token) round-trips untouched while genuine self-references are still swapped — the split-and-rejoin rewrite has no sentinel/placeholder to collide with', () => {
const fixture = [
'Claude Code embeds a literal NUL byte here: [] and a placeholder-shaped token <!--PLACEHOLDER--> in its prose.',
'',
'<runtime_compatibility>',
'- **Claude Code:** reference implementation',
'</runtime_compatibility>',
'',
'Claude Code again, after the block.',
].join('\n');
const out = applyClaudeCodeBrandSwap(fixture, 'Trae');
// Genuine self-references outside the block ARE swapped.
assert.ok(out.startsWith('Trae embeds'), 'leading self-reference swapped');
assert.ok(out.includes('Trae again, after the block.'), 'trailing self-reference swapped');
// The NUL byte survives verbatim, exactly once, with no corruption.
assert.ok(out.includes('[]'), 'NUL byte preserved verbatim');
assert.strictEqual(out.split('').length - 1, 1, 'NUL byte appears exactly once — not duplicated or leaked');
// The placeholder-shaped token survives verbatim, exactly once — proving
// there is no internal sentinel this content could collide with.
assert.ok(out.includes('<!--PLACEHOLDER-->'), 'placeholder-shaped token preserved verbatim');
assert.strictEqual((out.match(/<!--PLACEHOLDER-->/g) || []).length, 1, 'placeholder-shaped token appears exactly once — not duplicated or leaked');
// The protected block is untouched, including its interior "Claude Code" label.
const block = out.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(block.includes('**Claude Code:**'), 'block interior "Claude Code" left verbatim');
assert.ok(!block.includes('Trae'), 'block never gains the installing runtime\'s own brand name');
});
test('inside-AND-outside: a fixture with "Claude Code" both inside a <runtime_compatibility> block and in surrounding prose swaps only the outside occurrence', () => {
const fixture = [
'Claude Code is the host running this installer.',
'<runtime_compatibility>',
'- **Claude Code:** compared-runtime label, must stay verbatim',
'</runtime_compatibility>',
'This is still Claude Code speaking.',
].join('\n');
const out = applyClaudeCodeBrandSwap(fixture, 'Cursor');
assert.ok(out.includes('Cursor is the host running this installer.'), 'outside occurrence before the block is swapped');
assert.ok(out.includes('This is still Cursor speaking.'), 'outside occurrence after the block is swapped');
const block = out.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(block.includes('**Claude Code:**'), 'inside occurrence is preserved verbatim');
assert.ok(!block.includes('Cursor'), 'inside occurrence is never swapped');
});
describe('real corpus: gsd-core/workflows/execute-phase.md <runtime_compatibility> table', () => {
const FILE = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md');
const CONTENT = fs.readFileSync(FILE, 'utf8');
test('sanity: the source file has a <runtime_compatibility> block containing "Claude Code:" as a compared-runtime label', () => {
assert.ok(/<runtime_compatibility>/.test(CONTENT));
const block = CONTENT.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(/\*\*Claude Code:\*\*/.test(block));
});
test('Windsurf: the compared-runtime label "Claude Code:" is NOT swapped to "Windsurf:", but genuine self-references elsewhere ARE', () => {
const out = convertClaudeToWindsurfMarkdown(CONTENT);
const block = out.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(/\*\*Claude Code:\*\*/.test(block), 'comparison-table label preserved for Windsurf');
assert.ok(!/\*\*Windsurf:\*\*/.test(block), 'comparison table never mislabeled with the installing runtime\'s own name');
const outsideBlock = out.replace(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g, '');
assert.ok(/\bWindsurf\b/.test(outsideBlock), 'genuine self-references outside the block ARE branded to Windsurf');
assert.ok(!/\bClaude Code\b/.test(outsideBlock), 'no residual "Claude Code" self-reference survives outside the block');
});
test('Hermes: the compared-runtime label "Claude Code:" is NOT swapped to "Hermes Agent:", but genuine self-references elsewhere ARE', () => {
const out = convertClaudeToHermesMarkdown(CONTENT, { runtime: 'hermes' });
const block = out.match(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/)[0];
assert.ok(/\*\*Claude Code:\*\*/.test(block), 'comparison-table label preserved for Hermes');
assert.ok(!/\*\*Hermes Agent:\*\*/.test(block), 'comparison table never mislabeled with Hermes\'s own brand name');
const outsideBlock = out.replace(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g, '');
assert.ok(/\bHermes Agent\b/.test(outsideBlock), 'genuine self-references outside the block ARE branded to Hermes Agent');
});
});
});

View File

@@ -236,7 +236,7 @@
"gsd-core/workflows/docs-update.md": "39f288623a8f6f32",
"gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74",
"gsd-core/workflows/eval-review.md": "3e1d7829ed2ed494",
"gsd-core/workflows/execute-phase.md": "0d9ff2faecb72225",
"gsd-core/workflows/execute-phase.md": "6213f0d6e64dc524",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "67ebc93f51968cb6",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "82e6cfe1e1b1ec0e",
@@ -274,7 +274,7 @@
"gsd-core/workflows/onboard.md": "6f9e6c0b484271a9",
"gsd-core/workflows/pause-work.md": "3530607514b0ac00",
"gsd-core/workflows/plan-milestone-gaps.md": "bafdc6945cd2bd87",
"gsd-core/workflows/plan-phase.md": "801be4f6db080adb",
"gsd-core/workflows/plan-phase.md": "32c4a6cb4937cce3",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "4b0a2cb0f4f28179",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "090c31e22b1508fe",

View File

@@ -304,7 +304,7 @@
"gsd-core/workflows/docs-update.md": "e86d7d7e2e3dac6d",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "a86279dd98dd5c03",
"gsd-core/workflows/execute-phase.md": "b3267f23af6444b2",
"gsd-core/workflows/execute-phase.md": "a761ee37b32a6fe2",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",
@@ -342,7 +342,7 @@
"gsd-core/workflows/onboard.md": "20c28136423d40ac",
"gsd-core/workflows/pause-work.md": "5716362557f44ce4",
"gsd-core/workflows/plan-milestone-gaps.md": "1b43d12812f7bc1e",
"gsd-core/workflows/plan-phase.md": "846e7347c40cc643",
"gsd-core/workflows/plan-phase.md": "2aa913f604e21704",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "3bed01c3c906ac52",

View File

@@ -78,7 +78,7 @@
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json": "688ec62c13e08afe",
"gsd-core/references/edge-probe.md": "5687eba25a078561",
"gsd-core/references/execute-mvp-tdd.md": "a98a270a7ab126bc",
"gsd-core/references/execute-phase-between-wave-reset.md": "2c5cbdbc73cb053e",
"gsd-core/references/execute-phase-between-wave-reset.md": "4d6ea8e85e88b1fe",
"gsd-core/references/execute-phase-context-guard.md": "a5a1058d35806a8e",
"gsd-core/references/execute-phase-wave-guard.md": "b13d8860a76b7a41",
"gsd-core/references/executor-examples.md": "ba59243ed45c8ab1",
@@ -91,9 +91,9 @@
"gsd-core/references/gsd-run-resolver.md": "769d1472c989cd54",
"gsd-core/references/honest-verifier.md": "8815c9fc18c35719",
"gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f",
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/loop-hook-dispatch.md": "7135dd5d81246b0e",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profile-resolution.md": "8a1267752a05af27",
"gsd-core/references/model-profiles.md": "6012c3b53473f04f",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
@@ -139,7 +139,7 @@
"gsd-core/references/thinking-partner.md": "827c1badf3e6df41",
"gsd-core/references/ui-brand.md": "48717bcfcd63bd27",
"gsd-core/references/ui-consideration-probe.md": "7e019dfaae47f4c4",
"gsd-core/references/universal-anti-patterns.md": "6a1245050b21df01",
"gsd-core/references/universal-anti-patterns.md": "15ebe63b708d46b8",
"gsd-core/references/untrusted-input-boundary.md": "d33b80d4d348599a",
"gsd-core/references/user-profiling.md": "b50416fe57c1b321",
"gsd-core/references/user-story-template.md": "0cc50e06a144ff8a",
@@ -168,14 +168,14 @@
"gsd-core/templates/context.md": "69b01e7909ea3f66",
"gsd-core/templates/continue-here.md": "f522a51b6895fba8",
"gsd-core/templates/copilot-instructions.md": "aea34bc52ff548ea",
"gsd-core/templates/debug-subagent-prompt.md": "8c18a89e25929d8e",
"gsd-core/templates/debug-subagent-prompt.md": "855f2457818b4955",
"gsd-core/templates/dev-preferences.md": "95048a71063d980b",
"gsd-core/templates/discovery.md": "e4ab738326eb70e0",
"gsd-core/templates/discussion-log.md": "cac1b48ec0f4dcb8",
"gsd-core/templates/milestone-archive.md": "591b6decdc0c0e51",
"gsd-core/templates/milestone.md": "74d2f750ae9f4a9c",
"gsd-core/templates/phase-prompt.md": "213ccd947451ff2b",
"gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f",
"gsd-core/templates/planner-subagent-prompt.md": "d2a1373a1d8f53ae",
"gsd-core/templates/project.md": "ae1f68db042c2522",
"gsd-core/templates/requirements.md": "a44de4c2f146e473",
"gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0",
@@ -202,22 +202,22 @@
"gsd-core/workflows/add-todo.md": "5fe3ddc3227e0e22",
"gsd-core/workflows/ai-integration-phase.md": "ddab2912d025db65",
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
"gsd-core/workflows/audit-fix.md": "2eee4a0be82ef951",
"gsd-core/workflows/audit-milestone.md": "690289689e9eda7a",
"gsd-core/workflows/audit-fix.md": "2f21bd575bcedaf1",
"gsd-core/workflows/audit-milestone.md": "b96a78c6bd2e7690",
"gsd-core/workflows/audit-uat.md": "86d9131fccabf2ad",
"gsd-core/workflows/autonomous.md": "dd572ca89862e5ec",
"gsd-core/workflows/autonomous.md": "853dd93846332743",
"gsd-core/workflows/check-todos.md": "ea9a303c48a5d752",
"gsd-core/workflows/cleanup.md": "6c488059fc152a49",
"gsd-core/workflows/code-review-fix.md": "e829d3baf9901b54",
"gsd-core/workflows/code-review.md": "50a05ab8957bd05f",
"gsd-core/workflows/code-review-fix.md": "5e396494a79c3f19",
"gsd-core/workflows/code-review.md": "64a62562611f7f2b",
"gsd-core/workflows/complete-milestone.md": "f1866541148dc291",
"gsd-core/workflows/debug.md": "0289d3caa7252779",
"gsd-core/workflows/diagnose-issues.md": "16f2d2a85335641f",
"gsd-core/workflows/debug.md": "a8132ea4480a4730",
"gsd-core/workflows/diagnose-issues.md": "385096674965c043",
"gsd-core/workflows/discovery-phase.md": "6161c60d752d0058",
"gsd-core/workflows/discuss-phase-assumptions.md": "3a1e215890d2b3f4",
"gsd-core/workflows/discuss-phase-assumptions.md": "71b86e3cf85d8697",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "0aa052ae4ee70bf6",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "536e2fa842f1bc95",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "624db27cd84c4626",
"gsd-core/workflows/discuss-phase/modes/all.md": "fa70d79066562e54",
"gsd-core/workflows/discuss-phase/modes/analyze.md": "da0788f3be7f8105",
"gsd-core/workflows/discuss-phase/modes/auto.md": "af2d9d6ebb3f69c9",
@@ -230,17 +230,17 @@
"gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f",
"gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1",
"gsd-core/workflows/do.md": "e37540cc874fb782",
"gsd-core/workflows/docs-update.md": "4d6c06e611d83b6d",
"gsd-core/workflows/docs-update.md": "ba4cf926fc463cbe",
"gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8",
"gsd-core/workflows/eval-review.md": "f510e5762212dc6f",
"gsd-core/workflows/execute-phase.md": "371ab0a4dae7a049",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "ceb8758c22660c1a",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase.md": "e99263e6cfe3bdb2",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "14a27cc0828f59d3",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c9ad17d6cc6dfe45",
"gsd-core/workflows/execute-phase/steps/regression-gate.md": "5a78d5dfea6a911a",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md": "be84efbd71e1513e",
"gsd-core/workflows/execute-plan.md": "5a6b2176073db3fd",
"gsd-core/workflows/explore.md": "48770d68e8b9c132",
"gsd-core/workflows/execute-plan.md": "274b22f8e53c88d1",
"gsd-core/workflows/explore.md": "d931b4d85a74c795",
"gsd-core/workflows/extract-learnings.md": "e9e167c718949c0b",
"gsd-core/workflows/fast.md": "c801145115755524",
"gsd-core/workflows/forensics.md": "91961b811917c5c4",
@@ -251,19 +251,19 @@
"gsd-core/workflows/help/modes/default.md": "8874dac94eb68ae6",
"gsd-core/workflows/help/modes/full.md": "cad4eed3907e8a58",
"gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9",
"gsd-core/workflows/import.md": "a441445515bc2dd5",
"gsd-core/workflows/import.md": "c6a19584809ea635",
"gsd-core/workflows/inbox.md": "91aac6360e1a8672",
"gsd-core/workflows/ingest-docs.md": "ad496a4c423f1ab7",
"gsd-core/workflows/ingest-docs.md": "716276a2f4999ab7",
"gsd-core/workflows/insert-phase.md": "a0537c09191121f6",
"gsd-core/workflows/list-phase-assumptions.md": "2a6b6a5acfb7742c",
"gsd-core/workflows/list-seeds.md": "ad1d81683e48c4e1",
"gsd-core/workflows/list-workspaces.md": "1fe28ba52cee1394",
"gsd-core/workflows/manager.md": "b0322e4514de4c4d",
"gsd-core/workflows/map-codebase.md": "7db2e66647dd3c34",
"gsd-core/workflows/manager.md": "68e6e70c4ebe0986",
"gsd-core/workflows/map-codebase.md": "420caa00abbd73f5",
"gsd-core/workflows/milestone-summary.md": "353fffb60749d892",
"gsd-core/workflows/mvp-phase.md": "4d102523b5d81f95",
"gsd-core/workflows/new-milestone.md": "4988e2b33d02075d",
"gsd-core/workflows/new-project.md": "c596ec378d462fdb",
"gsd-core/workflows/new-milestone.md": "4dc1da729fd1438c",
"gsd-core/workflows/new-project.md": "291d623166b97754",
"gsd-core/workflows/new-workspace.md": "5c78c58ad84e886b",
"gsd-core/workflows/next.md": "8e1fa29751d96564",
"gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b",
@@ -271,28 +271,28 @@
"gsd-core/workflows/onboard.md": "3c50ed1f1fd07619",
"gsd-core/workflows/pause-work.md": "ae2d5789a95f70fe",
"gsd-core/workflows/plan-milestone-gaps.md": "c86cdc1964256b98",
"gsd-core/workflows/plan-phase.md": "79bc3a45f034d851",
"gsd-core/workflows/plan-phase.md": "2f0c5b0b66ebcf1d",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "9607e6d03e93c1c2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "62f8e4f3b475fe5f",
"gsd-core/workflows/plan-review-convergence.md": "277e3495fb982045",
"gsd-core/workflows/plan-review-convergence.md": "52c9175d8d04a6de",
"gsd-core/workflows/plant-seed.md": "f862f77fca983749",
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
"gsd-core/workflows/progress.md": "2641aa5457ab784f",
"gsd-core/workflows/quick.md": "731c20b5605bd319",
"gsd-core/workflows/quick.md": "01560a250a6040ea",
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
"gsd-core/workflows/remove-workspace.md": "8facde381657dd71",
"gsd-core/workflows/resume-project.md": "a0443839f1f83c2d",
"gsd-core/workflows/review.md": "d8c53e495b067afe",
"gsd-core/workflows/scan.md": "b28f65d88c522767",
"gsd-core/workflows/secure-phase.md": "a503dc469fd7a252",
"gsd-core/workflows/scan.md": "ebc3faaf1170dd12",
"gsd-core/workflows/secure-phase.md": "5284cfc143ad0e2c",
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
"gsd-core/workflows/settings-advanced.md": "49be159144d7f426",
"gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5",
"gsd-core/workflows/settings.md": "0845d073009a4619",
"gsd-core/workflows/ship.md": "f2c98eac4edcd333",
"gsd-core/workflows/settings.md": "9ced580679ed0255",
"gsd-core/workflows/ship.md": "9ec9f316622ccfd9",
"gsd-core/workflows/sketch-wrap-up.md": "f1ece50ac65ea281",
"gsd-core/workflows/sketch.md": "d5887983e62b574a",
"gsd-core/workflows/smart-entry.md": "47f5c5e8608e5f7a",
@@ -303,14 +303,14 @@
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
"gsd-core/workflows/thread.md": "5a6759e01763c2a6",
"gsd-core/workflows/transition.md": "bcfaad44668d07fd",
"gsd-core/workflows/ui-phase.md": "a45a9409c26699f4",
"gsd-core/workflows/ui-review.md": "bc0ae72c0e1cab94",
"gsd-core/workflows/ui-phase.md": "64a49ae4ec7e11a9",
"gsd-core/workflows/ui-review.md": "2f669a3f62913703",
"gsd-core/workflows/ultraplan-phase.md": "0d103bf2622436f7",
"gsd-core/workflows/undo.md": "791e0bf96d9a057f",
"gsd-core/workflows/update.md": "7499bb4cb2a3ce6f",
"gsd-core/workflows/validate-phase.md": "88edc724568337d3",
"gsd-core/workflows/validate-phase.md": "24be83176c7db9f9",
"gsd-core/workflows/verify-phase.md": "5c8d1305b47fbef4",
"gsd-core/workflows/verify-work.md": "7a9c9541d2d73fdc",
"gsd-core/workflows/verify-work.md": "3847a7ccd4bd88a8",
"hooks/gsd-check-update-worker.js": "7989cc2bedd1138d",
"hooks/gsd-check-update.js": "25f5ad726f76fc11",
"hooks/gsd-config-reload.js": "880b696458e85e9b",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "45d2f0d173c84e07",
"gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36",
"gsd-core/workflows/eval-review.md": "6dee8a1e40ececd4",
"gsd-core/workflows/execute-phase.md": "b6d1f7dbc3c81a4f",
"gsd-core/workflows/execute-phase.md": "38370aa7653a1c5b",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "36af8d91e4ae8b9c",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "98db1ba4c39cd784",
@@ -271,7 +271,7 @@
"gsd-core/workflows/onboard.md": "3c50ed1f1fd07619",
"gsd-core/workflows/pause-work.md": "be33f84dc1d4822f",
"gsd-core/workflows/plan-milestone-gaps.md": "d98e98486123eb97",
"gsd-core/workflows/plan-phase.md": "2d1f7925036bd901",
"gsd-core/workflows/plan-phase.md": "2b0110c2cd8de18a",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "c22ff5ea46de665a",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "d050d8d551ed1756",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "f13571f08e083356",
"gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad",
"gsd-core/workflows/eval-review.md": "37d545d4f0db4927",
"gsd-core/workflows/execute-phase.md": "80ffe75fda2a90ba",
"gsd-core/workflows/execute-phase.md": "8d197109e4d60522",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "c985a30317a1aa6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0a9e915170c7121c",
@@ -271,7 +271,7 @@
"gsd-core/workflows/onboard.md": "61f111302af0f404",
"gsd-core/workflows/pause-work.md": "c20d267e28ce92f0",
"gsd-core/workflows/plan-milestone-gaps.md": "26db7b9329b7ddc8",
"gsd-core/workflows/plan-phase.md": "c4d0d0558a3b116e",
"gsd-core/workflows/plan-phase.md": "ab5ecd1e6a256c11",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "778b73a8db6f7c32",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "619946c879f33b9d",

View File

@@ -233,7 +233,7 @@
"gsd-core/workflows/docs-update.md": "70f73cc8c27e0ca0",
"gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789",
"gsd-core/workflows/eval-review.md": "b28be79ef29f16fd",
"gsd-core/workflows/execute-phase.md": "d6bf589fb0370bc7",
"gsd-core/workflows/execute-phase.md": "7d88881ded1b575e",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "47ae5482f8e64100",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "15bca39a75c664be",
@@ -271,7 +271,7 @@
"gsd-core/workflows/onboard.md": "20c28136423d40ac",
"gsd-core/workflows/pause-work.md": "93fcc1c845da6396",
"gsd-core/workflows/plan-milestone-gaps.md": "7880866ee1caf923",
"gsd-core/workflows/plan-phase.md": "b0d5d582ef76c0ff",
"gsd-core/workflows/plan-phase.md": "0404dbc786881567",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "80b1ba493a9a967f",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "3fed4740a91d0443",

View File

@@ -573,6 +573,20 @@ describe('uninstall skills cleanup — hermes', () => {
// ─── Section 4: No Claude references leak into non-Claude runtimes ────────────
// #2284(b): a `<runtime_compatibility>...</runtime_compatibility>` block (e.g.
// execute-phase.md/plan-phase.md's "**Subagent spawning is runtime-specific:**"
// table) INTENTIONALLY retains "Claude Code" as a COMPARISON-RUNTIME LABEL —
// "- **Claude Code:** Uses `Agent(subagent_type=..., ...)`" documents how
// Claude Code behaves for a reader on ANY installed runtime; it is never a
// host-self-reference the install is supposed to rebrand away. Strip these
// blocks before the leak scan below so that legitimate retention doesn't trip
// the "zero Claude references" invariant — everything OUTSIDE a
// `runtime_compatibility` block is still held to zero, so an actual leaked
// self-reference elsewhere in the file still fails this test.
function stripRuntimeCompatibilityBlocks(content) {
return content.replace(/<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g, '');
}
for (const runtime of ['hermes', 'qwen']) {
describe(`no Claude references leak into ${runtime} install`, () => {
let tmpDir;
@@ -628,7 +642,9 @@ for (const runtime of ['hermes', 'qwen']) {
path.basename(f) !== 'CHANGELOG.md'
);
const leaks = allFiles.filter(f => {
const c = fs.readFileSync(f, 'utf8');
// #2284(b): exempt `<runtime_compatibility>` comparison-table content
// — see the exemption's rationale above the `for` loop.
const c = stripRuntimeCompatibilityBlocks(fs.readFileSync(f, 'utf8'));
return /\bCLAUDE\.md\b/.test(c) || /\bClaude Code\b/.test(c) || /\.claude\//.test(c);
}).map(f => path.relative(tmpDir, f));
assert.strictEqual(leaks.length, 0, `Leaking: ${leaks.join(', ')}`);