test(#3333): fold the runtime & install surface fix-* cluster — Wave 1 (#3341)

* test(#3333): fold the runtime & install surface fix-* cluster — Wave 1

Folds 11 legacy tests/fix-*.test.cjs regression files into their module's
main test suite: 6 folded into existing suites (host-integration-descriptors,
effort-surface-axis, trae-imperative-reference, hermes-skills-migration,
gsd-agent-isolation-guard), 5 renamed to become the module's sole suite
(cursor-hook-workspace-roots, cursor-subagent-isolation,
lint-compiled-artifact-sync, hooks-commonjs-marker,
shared-hooks-dir-resolution). All 195 test() blocks preserved with zero
drops; lint-test-file-count.cjs and eslint remain clean. No production code
changed. Wave 1 of 7 in #3315 (H3 of epic #3053).

* test(#3333): replace try/finally with t.after() in isolation-guard tests

CONTRIBUTING.md bans try/finally inside test bodies (masks failures, not an
approved pattern). The fold in the prior commit carried 27 instances forward
verbatim from the deleted fix-3045-dispatch-isolation-resolver.test.cjs into
an otherwise-clean file. Converts each to the approved per-test t.after()
cleanup pattern — same cleanup call, registered instead of finally-wrapped.
No assertion, fixture, or test-name change; test( count unchanged at 50.

Found by the Standards review pass on Wave 1 (#3333, H3 of epic #3053).

* fix(#3333): restore raw NUL byte mangled by the fold in hermes-skills-migration.test.cjs

The prior fold commit copied fix-2284-hermes-agent-delegate-task-projection's
"collision-robust" test via a text-based Read/Write pipeline, which silently
turned a raw NUL byte (0x00) embedded in two string literals into a regular
space character. That corrupted the test's actual purpose (proving a NUL
byte survives a string-rewrite operation untouched) and produced a genuine
gsd-test failure: `24 !== 1` for `out.split(' ').length`, because splitting
on a space finds every space in the sentence instead of the single NUL byte
the test meant to isolate.

Root-caused by diffing the raw bytes (via `git cat-file blob` + `cat -v`)
between the pre-fold source and the folded target — confirmed exactly two
bytes differ. Restored via a byte-precise patch (latin1 round-trip) touching
only those two lines; test( count and every other byte unchanged.

* fix(#3333): use \x00 escape sequence instead of a raw NUL byte in test fixture

The prior commit restored a byte-exact raw NUL byte matching the original
fix-2284 source, and the production function (applyClaudeCodeBrandSwap) was
confirmed correct in a standalone repro. But the same raw byte still failed
through gsd-test's remote pipeline. Root cause is upstream of gsd-core: some
step in that transfer path does not carry a raw 0x00 byte through untouched.

A raw embedded NUL byte was never necessary here — `\x00` as a 4-character
escape sequence in the source text produces the identical runtime character
(U+0000) without ever putting a raw byte in the tracked file, sidestepping
any byte-oriented transfer step. Applied at both call sites (the fixture
string and the split() delimiter). No behavior change; test( count unchanged
at 76.

* fix(#3333): harden copyWithPathReplacement against a source file vanishing mid-copy (TOCTOU)

Surfaced by this PR's own gsd-test run: tests/install-minimal-hooks.test.cjs
and tests/opencode-command-dir-plural.test.cjs intermittently crashed with
ENOENT reading gsd-core/workflows/zzz-e5-drift-fixture.md. Root cause is
unrelated to test-file consolidation — tests/planning-prompt-drift.test.cjs
writes that fixture directly into the real, shared gsd-core/workflows/ tree
(main() hardcodes its scan root to the real repo) and deletes it in
t.after(); copyWithPathReplacement's readdirSync-then-read loop has no
protection against the listed file vanishing before it gets there, so a
concurrently-running install path can crash entirely on what is otherwise a
completely benign race.

Fixed by skipping (not crashing on) a listed entry that no longer exists by
the time the loop reaches it. Added a regression test that deterministically
reproduces the race (readdirSync snapshot still lists the file; it is
deleted immediately after) and proves both outcomes: no throw, and the
vanished entry's destination is never partially written.

Per CLAUDE.md's no-defer rule, a defect surfaced while verifying this PR is
fixed inline rather than deferred — this overrides one-concern-per-PR.

* fix(#3333): fix third NUL-byte-mangled occurrence missed by prior fix passes

The fold originally mangled three raw-NUL-byte occurrences to spaces, not
two — the earlier byte-restore and escape-sequence commits both only
targeted the fixture string and the split() delimiter, missing
out.includes('[ ]') a few lines below (should read out.includes('[\x00]')).
A remote gsd-test run kept failing on this exact assertion even after both
prior fixes, which is what surfaced the miss. Verified via a standalone
repro using the file's real (not retyped) fixture content: all six
assertions in the collision-robust test now pass. Zero raw NUL bytes remain
in the file; test( count unchanged at 76.

* chore(#3333): add changeset for the copyWithPathReplacement TOCTOU fix

Fixed-type fragment for the production defect fixed inline in this PR
(bin/install.js's copyWithPathReplacement). Exempt from docs/ requirements
per CONTRIBUTING.md (only Added/Changed/Deprecated/Removed require it).

* chore(#3333): backfill changeset PR number (pr:0 -> pr:3341)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-10 19:02:34 -04:00
committed by GitHub
parent 7a7bf19fc1
commit 33fca50d8a
19 changed files with 1899 additions and 2035 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3341
---
**Installer no longer crashes when a source file disappears mid-copy.** `copyWithPathReplacement` used to throw an unhandled ENOENT if a listed workflow/command file was deleted between its directory listing and the actual read — a rare filesystem race that could abort an entire install. It now skips the vanished file and continues installing everything else. (#3333)

View File

@@ -7878,6 +7878,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
const srcPath = path.join(srcDir, entry.name);
const destPath = path.join(destDir, entry.name);
// #3333: srcPath was enumerated by readdirSync above, but a filesystem is not
// transactional — the file it named can vanish between listing and this read
// (a concurrent process, or another test in this suite writing/cleaning up a
// fixture inside this same real directory). Treat "gone by the time we get
// here" as benign and skip it, never a fatal crash of the whole install.
if (!entry.isDirectory() && !fs.existsSync(srcPath)) {
continue;
}
if (entry.isDirectory()) {
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot);
} else if (entry.name.endsWith('.md')) {

View File

@@ -1,7 +1,11 @@
// allow-test-rule: source-text-is-the-product (see #2481)
// allow-test-rule: source-text-is-the-product (see #2481, #2615)
// The final describe block asserts on gsd-core/workflows/review.md's text. A
// workflow .md IS what the runtime loads — its literal command lines are the
// deployed contract, and there is no runtime seam that executes review.md here.
// The #2615 matrix-parity block below is the same kind of contract assertion:
// docs/reference/host-integration-capability-matrix.md IS the cited source of
// truth for every descriptor axis (ADR-1239), so asserting a shipped axis
// value appears there and matches is a contract assertion, not a source grep.
// Every other block in this file is behavioral (CLI + module surface).
/**
@@ -37,6 +41,31 @@ const {
_HOST_INTEGRATION_VOCAB,
validateRuntimeBody,
} = require(path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'capability-validator.cjs'));
const registry = require(path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
// #2615: the host-integration capability matrix, normalized so CRLF checkouts
// (Windows autocrlf) don't break the row regexes below.
const MATRIX = path.join(REPO_ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
const MATRIX_TEXT = fs.readFileSync(MATRIX, 'utf-8').replace(/\r\n/g, '\n');
/** Extract a `## <host>` section body, stopping at the next top-level host heading. */
function matrixSection(host) {
const start = MATRIX_TEXT.indexOf(`\n## ${host}\n`);
if (start === -1) return null;
const rest = MATRIX_TEXT.slice(start + 1);
const end = rest.indexOf('\n## ');
return end === -1 ? rest : rest.slice(0, end);
}
/** Read the value cell of a `| <axis> | <value> | …` row. */
function matrixAxisValue(body, axis) {
const row = body.split(/\r?\n/).find((l) => l.startsWith(`| ${axis} |`));
return row ? row.split('|')[2].trim() : null;
}
const MATRIX_RUNTIMES = Object.keys(registry.runtimes).filter(
(id) => registry.runtimes[id]?.runtime?.hostIntegration,
);
/**
* A real shipped descriptor with one hostIntegration axis stripped.
@@ -427,3 +456,47 @@ describe('#2481 review workflow resolves effort per reviewer', () => {
assert.deepStrictEqual(argvEffort, ['claude', 'codex', 'opencode']);
});
});
describe('#2615: the matrix documents the effortSurface axis', () => {
test('the axes legend defines effortSurface and its vocabulary', () => {
const legendRow = MATRIX_TEXT.split(/\r?\n/).find((l) => l.startsWith('| `effortSurface` |'));
assert.ok(legendRow, 'the axes legend must define effortSurface (#2615)');
for (const member of ['`argv`', '`none`', '`undocumented`']) {
assert.ok(legendRow.includes(member),
`the legend must document the ${member} vocabulary member (#2615)`);
}
});
test('there is at least one runtime to check', () => {
// Guards the loops below against silently asserting nothing.
assert.ok(MATRIX_RUNTIMES.length >= 18, `expected the full runtime corpus, got ${MATRIX_RUNTIMES.length}`);
});
for (const id of MATRIX_RUNTIMES) {
describe(`runtime: ${id}`, () => {
test('has a matrix section', () => {
assert.ok(matrixSection(id), `${id}: every installed runtime needs a matrix section (ADR-1239)`);
});
test('documents effortSurface, and the value matches the descriptor', () => {
const body = matrixSection(id);
assert.ok(body, `${id}: missing matrix section`);
const documented = matrixAxisValue(body, 'effortSurface');
assert.ok(documented, `${id}: the matrix must carry an effortSurface row (#2615)`);
const declared = registry.runtimes[id].runtime.hostIntegration.effortSurface;
if (declared === undefined) {
// kimi-code declares no value: its mechanism (`/effort`) is interactive-only
// and neither `argv` nor `none` describes it. The matrix must say so rather
// than invent a value.
assert.match(documented, /not declared/i,
`${id}: an absent descriptor value must be documented as absent, not guessed (#2615)`);
} else {
assert.equal(documented, declared,
`${id}: the matrix effortSurface value must match the shipped descriptor`);
}
});
});
}
});

View File

@@ -1,895 +0,0 @@
// 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

@@ -1,91 +0,0 @@
/**
* #2598 — the OpenCode descriptor declared background/concurrent subagent
* dispatch that OpenCode does not actually provide by default.
*
* `capabilities/opencode/capability.json` carried
* `runtime.hostIntegration.dispatch.background: true` and
* `dispatch.backgroundDispatch: true`. OpenCode's native subagent dispatch
* (Task tool / `@`-mention / `subtask`) is synchronous: the `background`
* parameter is hidden from the model behind the opt-in
* `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag, which defaults to false
* (`enabledByExperimental(...)` over a `bool()` that defaults false), and the
* session loop still `tasks.pop()`s one subtask at a time (upstream #14195,
* #29638 — the latter still open).
*
* `negotiateHostCapabilities` and every `degradationFor`/`shouldFlattenDispatch`
* consumer TRUSTS these per-field values, so declaring a capability the host
* lacks overstates it — the opposite of the fail-closed posture the negotiation
* exists to enforce.
*
* History note: these fields were flipped to `true` by #2087 citing a reading of
* OpenCode v1.17 as "background subagents enabled by default in all modes".
* That reading does not hold against current upstream `dev`, where the flag is
* opt-in. This test pins the corrected values so a future descriptor edit cannot
* silently re-assert an unsupported capability.
*/
// allow-test-rule: source-text-is-the-product #2598 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe } = 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 DESCRIPTOR = path.join(ROOT, 'capabilities', 'opencode', 'capability.json');
const MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
function opencodeDispatch() {
const parsed = JSON.parse(fs.readFileSync(DESCRIPTOR, 'utf8'));
return parsed.runtime.hostIntegration.dispatch;
}
describe('#2598: OpenCode does not declare background/concurrent subagent dispatch', () => {
test('descriptor declares background: false', () => {
assert.equal(
opencodeDispatch().background,
false,
'OpenCode subagent dispatch is synchronous unless an experimental opt-in flag is set',
);
});
test('descriptor declares backgroundDispatch: false', () => {
assert.equal(
opencodeDispatch().backgroundDispatch,
false,
'concurrent dispatch requires an opt-in flag, so it must not be declared as available',
);
});
test('the capabilities that ARE real are left intact', () => {
// Narrow the blast radius: this fix must not quietly downgrade neighbouring
// sub-fields that were never in question.
const d = opencodeDispatch();
assert.equal(d.namedDispatch, true, 'named subagent dispatch is genuinely supported');
assert.equal(d.subagentToolkit, 'full', 'the general subagent has full tool access');
assert.equal(d.isolation, 'orchestrator-worktree',
'isolation is orchestrator-managed via `opencode run --dir`, unaffected by #2598');
});
test('the host-integration matrix agrees with the descriptor', () => {
// ADR-1239 designates the matrix the deployment source-of-truth; a
// descriptor/matrix disagreement is how this defect survived in the first
// place (the matrix said true, the ADR binding table said false).
const matrix = fs.readFileSync(MATRIX, 'utf8');
const section = matrix.slice(matrix.indexOf('## opencode'));
const end = section.indexOf('\n## ');
const opencodeSection = end === -1 ? section : section.slice(0, end);
for (const field of ['dispatch.background', 'dispatch.backgroundDispatch']) {
const row = opencodeSection.split('\n').find((l) => l.startsWith(`| ${field} |`));
assert.ok(row, `matrix must document ${field} for opencode`);
const value = row.split('|')[2].trim();
assert.equal(value, 'false', `matrix ${field} must match the descriptor`);
}
});
});

View File

@@ -1,224 +0,0 @@
/**
* #2603 — `docs/reference/host-integration-capability-matrix.md` documented 18 of the
* 19 installed runtimes but had no `## kimi-code` section, so kimi-code's
* `runtime.hostIntegration` axes shipped with no cited source and no evidence quote.
*
* Sourcing each axis independently (the issue's explicit requirement — "Do not copy
* `kimi`'s section", they are distinct products) showed three axis values had been
* inherited from the Python `kimi` descriptor rather than sourced for Kimi Code CLI:
*
* - `embeddingMode: imperative` → `declarative`. Kimi Code plugins are a
* `kimi.plugin.json` manifest plus markdown Skills; "Plugins are configuration and
* markdown only" with no in-process programmatic API (docs/en/customization/plugins.md).
* Same shape as codex, which is `declarative`.
* - `dispatch.nested: false` → `true`. The `coder` built-in "can dispatch its own
* nested sub-agents when a task decomposes naturally" (docs/en/customization/agents.md).
* The Python `kimi` CLI genuinely prohibits nesting; Kimi Code does not.
* - `dispatch.maxDepth: 1` → `'undocumented'`. Nesting is documented but no depth
* bound is published, so the fail-closed sentinel applies rather than a guessed 1.
*
* `dispatch.namedDispatch` deliberately stays `false`: GSD's kimi-code artifact layout
* installs Agent Skills only (no `agents` kind), so no named GSD subagent is registered
* with the host and `resolveDispatchType` maps every role onto coder/explore/plan.
* Flipping it without also shipping agent files would reintroduce the dispatch failure
* recorded in docs/migration/kimi-to-kimi-code.md.
*
* This is the same defect class as #2598 (a descriptor axis asserting something the
* host docs contradict), and takes the same countermeasure: pin the corrected values
* AND require the matrix to agree with the descriptor, because a descriptor/matrix
* disagreement is how the gap survived.
*/
// allow-test-rule: source-text-is-the-product #2603 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe } = 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 DESCRIPTOR = path.join(ROOT, 'capabilities', 'kimi-code', 'capability.json');
const MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
const {
profileOf,
negotiateHostCapabilities,
} = require(path.join(ROOT, 'gsd-core/bin/lib/host-integration.cjs'));
function kimiCodeAxes() {
return JSON.parse(fs.readFileSync(DESCRIPTOR, 'utf8')).runtime.hostIntegration;
}
/** Extract the `## <host>` section body, stopping at the next top-level host heading. */
function matrixSection(host) {
const matrix = fs.readFileSync(MATRIX, 'utf8');
const start = matrix.indexOf(`\n## ${host}\n`);
if (start === -1) return null;
const rest = matrix.slice(start + 1);
const end = rest.indexOf('\n## ');
return end === -1 ? rest : rest.slice(0, end);
}
/** Read the `| <axis> | <value> | …` cell out of a matrix section. */
function matrixValue(section, axis) {
const row = section.split('\n').find((l) => l.startsWith(`| ${axis} |`));
return row ? row.split('|')[2].trim() : null;
}
describe('#2603: the host-integration matrix documents kimi-code', () => {
test('a `## kimi-code` section exists', () => {
assert.ok(
matrixSection('kimi-code'),
'the matrix is the deployment source-of-truth for every installed runtime; kimi-code must have a section',
);
});
test('every hostIntegration axis kimi-code declares is documented in the matrix', () => {
const section = matrixSection('kimi-code');
const axes = kimiCodeAxes();
const scalarAxes = Object.keys(axes).filter((k) => k !== 'dispatch');
for (const axis of scalarAxes) {
assert.ok(
matrixValue(section, axis),
`matrix must document the "${axis}" axis for kimi-code`,
);
}
// `builtInSubagents` is a GSD-side list, not a negotiated axis — the matrix
// documents it in prose, not as its own row.
const dispatchAxes = Object.keys(axes.dispatch).filter((k) => k !== 'builtInSubagents');
for (const axis of dispatchAxes) {
assert.ok(
matrixValue(section, `dispatch.${axis}`),
`matrix must document the "dispatch.${axis}" sub-axis for kimi-code`,
);
}
});
test('the matrix values agree with the shipped descriptor', () => {
const section = matrixSection('kimi-code');
const axes = kimiCodeAxes();
for (const axis of Object.keys(axes).filter((k) => k !== 'dispatch')) {
assert.equal(
matrixValue(section, axis),
String(axes[axis]),
`matrix "${axis}" must match the descriptor`,
);
}
for (const axis of Object.keys(axes.dispatch).filter((k) => k !== 'builtInSubagents')) {
assert.equal(
matrixValue(section, `dispatch.${axis}`),
String(axes.dispatch[axis]),
`matrix "dispatch.${axis}" must match the descriptor`,
);
}
});
test('the kimi-code section is sourced independently of the kimi section', () => {
// The two are distinct products (Python kimi-cli vs TypeScript Kimi Code CLI);
// the issue's central requirement is that kimi's section was NOT copied. The
// check is scoped to the axis ROWS — the section's prose intro deliberately
// names kimi's Python API to draw the contrast, which is the opposite of a copy.
const rows = matrixSection('kimi-code')
.split('\n')
.filter((l) => l.startsWith('| ') && !l.startsWith('| Axis |') && !l.startsWith('|---'));
assert.ok(rows.length >= 11, 'expected a row per hostIntegration axis');
for (const row of rows) {
assert.ok(
!row.includes('kimi_cli'),
`kimi-code axis row must not cite the Python kimi-cli: ${row.slice(0, 60)}`,
);
assert.ok(
!row.includes('moonshotai.github.io/kimi-cli'),
`kimi-code axis row must not cite kimi-cli docs: ${row.slice(0, 60)}`,
);
}
assert.ok(
rows.some((r) => r.includes('kimi-code/blob/main/docs')),
'kimi-code axes must cite the Kimi Code CLI docs',
);
});
});
describe('#2603: axis values inherited from the Python kimi descriptor are corrected', () => {
test('embeddingMode is declarative — plugins expose no in-process API', () => {
assert.equal(kimiCodeAxes().embeddingMode, 'declarative');
});
test('kimi-code therefore classifies as the declarative-cli profile', () => {
assert.equal(profileOf(kimiCodeAxes()), 'declarative-cli');
});
test('dispatch.nested is true — the coder built-in dispatches nested sub-agents', () => {
assert.equal(kimiCodeAxes().dispatch.nested, true);
});
test('dispatch.maxDepth is the undocumented sentinel, not a guessed integer', () => {
assert.equal(kimiCodeAxes().dispatch.maxDepth, 'undocumented');
});
test('namedDispatch stays false — GSD installs no agent files for this host', () => {
// Guard against a well-meaning "the docs say custom agents exist" edit: flipping
// this makes resolveDispatchType return `gsd-planner` unchanged, which kimi-code
// cannot dispatch (docs/migration/kimi-to-kimi-code.md).
assert.equal(kimiCodeAxes().dispatch.namedDispatch, false);
});
test('the undocumented maxDepth sentinel is reported as a sentinel, not as malformed', () => {
// Surfaced by this change: maxDepth was the ONE dispatch sub-axis with no
// sentinel-specific warning, so the documented fail-closed value was reported
// as "missing or not a number" — indistinguishable from a genuinely broken
// descriptor. kimi-code would have been the sixth runtime to hit that path.
const { warnings } = negotiateHostCapabilities(kimiCodeAxes());
assert.ok(
warnings.some((w) => w.includes('dispatch.maxDepth is undocumented')),
`expected a maxDepth sentinel warning, got: ${JSON.stringify(warnings)}`,
);
assert.ok(
!warnings.some((w) => w.includes('maxDepth is missing or not a number')),
'the documented sentinel must not be reported as a malformed value',
);
});
test('a genuinely malformed maxDepth is still reported as malformed', () => {
// Boundary: the sentinel carve-out must not swallow the real error case.
const axes = kimiCodeAxes();
const malformed = { ...axes, dispatch: { ...axes.dispatch, maxDepth: 'not-a-number' } };
const { warnings } = negotiateHostCapabilities(malformed);
assert.ok(
warnings.some((w) => w.includes('maxDepth is missing or not a number')),
`expected the malformed-value warning, got: ${JSON.stringify(warnings)}`,
);
});
test('both maxDepth paths still degrade the effective value closed to 0', () => {
const axes = kimiCodeAxes();
assert.equal(negotiateHostCapabilities(axes).effective.dispatch.maxDepth, 0);
const malformed = { ...axes, dispatch: { ...axes.dispatch, maxDepth: 'not-a-number' } };
assert.equal(negotiateHostCapabilities(malformed).effective.dispatch.maxDepth, 0);
});
test('the axes that were already correct are left intact', () => {
const axes = kimiCodeAxes();
assert.equal(axes.commandSurface, 'slash-file');
assert.equal(axes.modelMode, 'passive');
assert.equal(axes.hookBus, 'host');
assert.equal(axes.stateIO, 'filesystem');
assert.equal(axes.transport, 'mcp');
assert.equal(axes.runtime, 'node');
assert.equal(axes.dispatch.background, true);
assert.equal(axes.dispatch.backgroundDispatch, true);
assert.equal(axes.dispatch.subagentToolkit, 'built-in-only');
assert.equal(axes.dispatch.isolation, 'orchestrator-worktree');
});
});

View File

@@ -1,99 +0,0 @@
// allow-test-rule: source-text-is-the-product #2615 — the host-integration matrix
// IS the cited source of truth for every descriptor axis (ADR-1239); asserting that a
// shipped axis value appears there, and matches, is a contract assertion.
/**
* Regression test for #2615 — `effortSurface` was a shipped `hostIntegration` axis
* with NO presence in the matrix that is supposed to be its cited source of truth.
*
* #2481 added the axis and wrote docs-sourced values into 18 descriptors
* (`claude`/`codex`/`opencode` -> `argv`, 15 others -> `undocumented`) but never
* touched `docs/reference/host-integration-capability-matrix.md`: the axes legend
* omitted it and not one per-runtime table carried a row. `src/host-integration.cts`
* states "every value is documented or explicitly 'undocumented'" — for this axis
* that was false for every runtime.
*
* This test is deliberately GENERIC rather than a hardcoded list: it derives the
* runtimes from the registry, so a runtime added later fails here until its matrix
* row exists. That is the ratchet the original gap needed — #2481 added an axis and
* nothing caught the missing documentation.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
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 MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
const registry = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
// Normalize CRLF so the row regexes hold on a Windows autocrlf checkout.
const MATRIX_TEXT = fs.readFileSync(MATRIX, 'utf-8').replace(/\r\n/g, '\n');
/** Extract a `## <host>` section body, stopping at the next top-level host heading. */
function section(host) {
const start = MATRIX_TEXT.indexOf(`\n## ${host}\n`);
if (start === -1) return null;
const rest = MATRIX_TEXT.slice(start + 1);
const end = rest.indexOf('\n## ');
return end === -1 ? rest : rest.slice(0, end);
}
/** Read the value cell of a `| <axis> | <value> | …` row. */
function axisValue(body, axis) {
const row = body.split(/\r?\n/).find((l) => l.startsWith(`| ${axis} |`));
return row ? row.split('|')[2].trim() : null;
}
const RUNTIMES = Object.keys(registry.runtimes).filter(
(id) => registry.runtimes[id]?.runtime?.hostIntegration,
);
describe('#2615: the matrix documents the effortSurface axis', () => {
test('the axes legend defines effortSurface and its vocabulary', () => {
const legendRow = MATRIX_TEXT.split(/\r?\n/).find((l) => l.startsWith('| `effortSurface` |'));
assert.ok(legendRow, 'the axes legend must define effortSurface (#2615)');
for (const member of ['`argv`', '`none`', '`undocumented`']) {
assert.ok(legendRow.includes(member),
`the legend must document the ${member} vocabulary member (#2615)`);
}
});
test('there is at least one runtime to check', () => {
// Guards the loops below against silently asserting nothing.
assert.ok(RUNTIMES.length >= 18, `expected the full runtime corpus, got ${RUNTIMES.length}`);
});
for (const id of RUNTIMES) {
describe(`runtime: ${id}`, () => {
test('has a matrix section', () => {
assert.ok(section(id), `${id}: every installed runtime needs a matrix section (ADR-1239)`);
});
test('documents effortSurface, and the value matches the descriptor', () => {
const body = section(id);
assert.ok(body, `${id}: missing matrix section`);
const documented = axisValue(body, 'effortSurface');
assert.ok(documented, `${id}: the matrix must carry an effortSurface row (#2615)`);
const declared = registry.runtimes[id].runtime.hostIntegration.effortSurface;
if (declared === undefined) {
// kimi-code declares no value: its mechanism (`/effort`) is interactive-only
// and neither `argv` nor `none` describes it. The matrix must say so rather
// than invent a value.
assert.match(documented, /not declared/i,
`${id}: an absent descriptor value must be documented as absent, not guessed (#2615)`);
} else {
assert.equal(documented, declared,
`${id}: the matrix effortSurface value must match the shipped descriptor`);
}
});
});
}
});

View File

@@ -1,265 +0,0 @@
'use strict';
/**
* Regression tests for #2658 — Trae runtime not detected in workflow
* runtime-detection blocks (falls back to claude), and the install-time
* `CLAUDE.md` path rewrite mutilates the claude fallback into a malformed
* path instead of resolving to the Trae rules file.
*
* Defects collided (see
* .gsd/bug/fix-2658-trae-runtime-not-detected-falls-back-to-/10-diagnosis.md):
*
* 1. `gsd-core/workflows/new-project.md` AND `gsd-core/workflows/ingest-docs.md`
* (found during this remediation — same pattern, same gap, not just
* new-project.md as originally reported) never recognized trae (path
* `/.trae/` or env `TRAE_CONFIG_DIR`) in their runtime-detection blocks —
* fell through to `RUNTIME=claude`.
* 2. The `trae.js` entry in `bin/install.js`'s `RUNTIME_CONTENT_DISPATCH`
* replaced bare `CLAUDE.md` first, leaving a stale `.claude/` prefix:
* `.claude/CLAUDE.md` -> `.claude/.trae/rules/`.
* 3. `convertClaudeToTraeMarkdown` (mirrored in `bin/install.js` and
* `src/runtime-artifact-conversion.cts`) had the same class of bug but a
* DIFFERENT wrong output (`.trae/.trae/rules/`), because its generic
* `.claude/` -> `.trae/` rewrite ran after the bare `CLAUDE.md` rewrite
* and re-mutated the leftover prefix.
* 4. `capabilities/trae/capability.json` didn't declare
* `hostBehaviors.projectInstructionFile`, so even a correctly-detected
* trae runtime resolved to the generic `AGENTS.md` default via
* `getProjectInstructionFile`.
* 5. Found by the end-to-end install test below, one level deeper than the
* static trace: `copyWithPathReplacement` (bin/install.js) runs a
* GENERIC `~/.claude/` / `$HOME/.claude/` / `./.claude/` -> runtime-dir
* rewrite on every .md file BEFORE calling `convertClaudeToTraeMarkdown`,
* substituting a `pathPrefix` the converter is never given (it differs
* per install: relative for a project-local install, an arbitrary
* absolute path for a local install rooted elsewhere, `~/.trae/` for a
* global one). The converter's `.claude/CLAUDE.md`-specific patterns
* (defect 3's fix) never fire on that already-rewritten text, and the
* bare fallback still doubles the prefix — a first attempt at fixing
* this handled only the `./.trae/CLAUDE.md` shape and missed the
* `~/.claude/` / `$HOME/.claude/` forms `gsd-core/workflows/profile-user.md`
* actually uses, caught by row 12 (the real spawned install) below on a
* second run. Fixed with a prefix-preserving pattern (capture whatever
* precedes a `.trae/` tail, keep it, fix only the filename suffix)
* instead of assuming one fixed shape.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
process.env['GSD_TEST_MODE'] = '1';
const { getProjectInstructionFile } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
const { convertClaudeToTraeMarkdown } = require('../bin/install.js');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
const { runMinimalInstall, walk } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const MALFORMED_SINGLE = '.claude/.trae/rules';
const MALFORMED_DOUBLE = '.trae/.trae/rules';
const EXPECTED_PATH = '.trae/rules/rules.md';
describe('#2658 acceptance criterion 2: getProjectInstructionFile resolves trae to a concrete file', () => {
test('trae maps to .trae/rules/rules.md (not the generic AGENTS.md default)', () => {
assert.strictEqual(getProjectInstructionFile('trae'), EXPECTED_PATH);
});
test('capability descriptor declares the same path getProjectInstructionFile returns', () => {
const cap = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'capabilities', 'trae', 'capability.json'), 'utf8'),
);
assert.strictEqual(cap.runtime.hostBehaviors.projectInstructionFile, EXPECTED_PATH);
assert.strictEqual(getProjectInstructionFile('trae'), cap.runtime.hostBehaviors.projectInstructionFile);
});
test('the declared path is a concrete file, not a bare directory (acceptance criterion 2)', () => {
assert.ok(!EXPECTED_PATH.endsWith('/'), 'must not be directory-terminated');
assert.ok(/\.md$/.test(EXPECTED_PATH), 'must name a concrete markdown file');
});
});
describe('#2658: convertClaudeToTraeMarkdown never mutilates the CLAUDE.md path (bin/install.js)', () => {
const cases = [
['bare CLAUDE.md', 'See CLAUDE.md for details.'],
['./CLAUDE.md', 'Read ./CLAUDE.md before starting.'],
['backtick-wrapped `CLAUDE.md`', 'The file `CLAUDE.md` is authoritative.'],
['the exact reported-bug input: .claude/CLAUDE.md', 'Fallback path is .claude/CLAUDE.md by default.'],
['backtick-wrapped .claude/CLAUDE.md', 'Fallback: `.claude/CLAUDE.md`.'],
['./.claude/CLAUDE.md', 'From root: ./.claude/CLAUDE.md'],
];
for (const [label, input] of cases) {
test(`${label} -> ${EXPECTED_PATH}, no malformed output`, () => {
const out = convertClaudeToTraeMarkdown(input);
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
assert.ok(out.includes(EXPECTED_PATH), `output must contain "${EXPECTED_PATH}": ${out}`);
});
}
test('fast-check property: any surrounding text around .claude/CLAUDE.md never yields a malformed path', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 40 }),
fc.string({ maxLength: 40 }),
(prefix, suffix) => {
const content = `${prefix}.claude/CLAUDE.md${suffix}`;
const out = convertClaudeToTraeMarkdown(content);
assert.ok(!out.includes(MALFORMED_SINGLE));
assert.ok(!out.includes(MALFORMED_DOUBLE));
},
),
{ numRuns: 200 },
);
});
});
describe('#2658 defect 5: post-generic-rewrite ".trae/"-prefixed forms preserve their prefix instead of doubling it', () => {
// These simulate the text `copyWithPathReplacement`'s generic `~/.claude/` /
// `$HOME/.claude/` / `./.claude/` -> runtime-dir pass hands to
// convertClaudeToTraeMarkdown — the converter never sees the original
// `.claude/`-prefixed source in this pipeline, only these already-rewritten
// shapes. A fixed-shape patch that only handled the local relative form
// left the local-install-absolute-path and global tilde forms broken.
const cases = [
['local relative (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md', './.trae/rules/rules.md'],
[
'local install absolute path (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/rules/rules.md',
],
['global tilde (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md', '~/.trae/rules/rules.md'],
['backtick-wrapped local relative', '`./.trae/CLAUDE.md`', '`./.trae/rules/rules.md`'],
];
for (const [label, input, expected] of cases) {
test(`${label} -> prefix preserved, no malformed path`, () => {
const out = convertClaudeToTraeMarkdown(input);
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
assert.strictEqual(out, expected);
});
}
test('fast-check property: any arbitrary path ending in .trae/ never yields a doubled prefix', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
(prefix) => {
const content = `${prefix}.trae/CLAUDE.md`;
const out = convertClaudeToTraeMarkdown(content);
assert.ok(!out.includes(MALFORMED_SINGLE));
assert.ok(!out.includes(MALFORMED_DOUBLE));
},
),
{ numRuns: 200 },
);
});
});
describe('#2658 output parity: bin/install.js vs runtime-artifact-conversion.cjs convertClaudeToTraeMarkdown (#2094 mirror)', () => {
// Parity must hold for the pre-existing reported-bug input AND for every
// arbitrary-prefix ".trae/"-tail shape the prefix-preserving regex
// (bin/install.js:2747-2748, mirrored byte-for-byte at
// src/runtime-artifact-conversion.cts:1357-1358) was added to handle. A
// change to only one copy of that regex would otherwise pass every other
// test in this file — none of the defect-5 cases above call the mirror —
// while silently diverging from the other copy.
const parityCases = [
['the reported-bug input (bare .claude/ prefix)', 'Fallback path is .claude/CLAUDE.md by default.'],
['local relative prefix (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md'],
[
'nested project-path absolute prefix (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
],
['global tilde prefix (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md'],
['$HOME-variable prefix (post "$HOME/.claude/" -> "$HOME/.trae/" rewrite)', '$HOME/.trae/CLAUDE.md'],
['backtick-wrapped local relative prefix', '`./.trae/CLAUDE.md`'],
];
for (const [label, input] of parityCases) {
test(`identical output for ${label}`, () => {
assert.strictEqual(
convertClaudeToTraeMarkdown(input),
runtimeArtifactConversion.convertClaudeToTraeMarkdown(input),
);
});
}
test('fast-check property: any arbitrary ".trae/"-tail path produces identical output in both implementations', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
(prefix) => {
const content = `${prefix}.trae/CLAUDE.md`;
assert.strictEqual(
convertClaudeToTraeMarkdown(content),
runtimeArtifactConversion.convertClaudeToTraeMarkdown(content),
);
},
),
{ numRuns: 200 },
);
});
});
describe('#2658: end-to-end --trae install never emits the malformed path (acceptance criterion 1)', () => {
test('local install: no emitted .md/.js/.cjs file contains the malformed strings; the rules file is concrete', () => {
const { configDir, root } = runMinimalInstall({ runtime: 'trae', scope: 'local' });
try {
const files = walk(configDir)
.filter((f) => /\.(md|js|cjs)$/.test(f))
// gsd-core/CHANGELOG.md is excluded by exact relative path (not a blanket
// .md skip — the emitted agent/command/workflow markdown this gate exists
// to guard stays fully scanned). CHANGELOG.md legitimately QUOTES the
// malformed `.claude/.trae/rules` / `.trae/.trae/rules` strings while
// documenting the #2658 fix itself (#3006) — that historical-value
// citation is not a regression of the installer's actual output. Verified
// empirically: excluding only this one file drops the hit count to zero
// across all 620 other emitted files.
.filter((f) => f.split(path.sep).join('/').indexOf('gsd-core/CHANGELOG.md') === -1);
assert.ok(files.length > 0, 'expected at least one emitted .md/.js/.cjs file');
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
assert.ok(!content.includes(MALFORMED_SINGLE), `${file} must not contain "${MALFORMED_SINGLE}"`);
assert.ok(!content.includes(MALFORMED_DOUBLE), `${file} must not contain "${MALFORMED_DOUBLE}"`);
}
} finally {
cleanup(root);
}
});
});
describe('#2658 acceptance criterion 3: new-project.md / ingest-docs.md detect trae before falling back to claude', () => {
const workflowsDir = path.join(REPO_ROOT, 'gsd-core', 'workflows');
test('new-project.md recognizes /.trae/ path and TRAE_CONFIG_DIR before the claude fallback', () => {
const content = fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8');
const pathBlock = content.match(/Derive `RUNTIME`[\s\S]*?Otherwise → `RUNTIME=claude`/);
assert.ok(pathBlock, 'runtime-detection path block must exist');
assert.ok(
/Path contains `\/\.trae\/` → `RUNTIME=trae`/.test(pathBlock[0]),
'path-based detection must recognize /.trae/ before the claude fallback',
);
const envBlock = content.match(/if \[ -n "\$CODEX_HOME" \][\s\S]*?else RUNTIME="claude"; fi/);
assert.ok(envBlock, 'env-var fallback block must exist');
assert.ok(
/TRAE_CONFIG_DIR/.test(envBlock[0]),
'env-var fallback must recognize TRAE_CONFIG_DIR before the claude fallback',
);
});
test('ingest-docs.md carries the same trae detection (found during this remediation, not just new-project.md)', () => {
const content = fs.readFileSync(path.join(workflowsDir, 'ingest-docs.md'), 'utf8');
const block = content.match(/\*\*Detect runtime\*\*[\s\S]*?else → `RUNTIME=claude`/);
assert.ok(block, 'runtime-detection block must exist');
assert.ok(
/`\/\.trae\/` → `RUNTIME=trae`/.test(block[0]),
'ingest-docs.md must also recognize /.trae/ before the claude fallback',
);
assert.ok(/TRAE_CONFIG_DIR/.test(content), 'env-var fallback mention must include TRAE_CONFIG_DIR');
});
});

View File

@@ -1,317 +0,0 @@
'use strict';
/**
* #3045 follow-up (two-review convergence: "the guard is fail-open in the
* default install") — CORE REDESIGN coverage for the sentinel WRITE side.
*
* Seam: `gsd-tools.cjs query dispatch-isolation` (routeDispatchIsolation) is
* now the SOLE, unconditional write path — it persists the resolved
* isolation decision (mode + harnessFlag + phase/plan identifiers) as a side
* effect of resolving it, so the workflow cannot learn ISOLATION without also
* recording it. `record-dispatch-isolation` (routeRecordDispatchIsolation)
* remains as an explicit fallback/testable primitive and shares the exact
* same atomic-write implementation.
*
* Every test here drives the REAL gsd-tools.cjs CLI (via runGsdTools) and
* asserts on the sentinel file it actually wrote, parsed as JSON — no
* fixture-text/source-string assertions.
*/
process.env.GSD_TEST_MODE = '1';
const { describe, test } = 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 { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
const { SENTINEL_RELATIVE_PATH, readSentinel } = require('../hooks/lib/isolation-sentinel.js');
const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
function sentinelFile(dir) {
return path.join(dir, SENTINEL_RELATIVE_PATH);
}
function readSentinelRaw(dir) {
return JSON.parse(fs.readFileSync(sentinelFile(dir), 'utf-8'));
}
describe('#3045 CORE REDESIGN — dispatch-isolation records as an unconditional side effect', () => {
test('a plain --raw query with no explicit isolation-record verb still writes the sentinel', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
assert.equal(fs.existsSync(sentinelFile(dir)), false, 'precondition: no sentinel yet');
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '7'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(result.output.trim(), 'harness-worktree');
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'harness-worktree');
assert.equal(sentinel.harness_flag, 'isolation="worktree"');
assert.equal(sentinel.phase, '7');
assert.equal(sentinel.plan, null);
assert.equal(typeof sentinel.written_at, 'number');
} finally {
cleanup(dir);
}
});
test('--json output and the recorded sentinel agree on isolation + harnessFlag', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'dispatch-isolation', '--json', '--phase', '3', '--plan', 'plan-b'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
const parsed = JSON.parse(result.output);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, parsed.isolation);
assert.equal(sentinel.harness_flag, parsed.harnessFlag);
assert.equal(sentinel.phase, '3');
assert.equal(sentinel.plan, 'plan-b');
} finally {
cleanup(dir);
}
});
test('--force-isolation none overrides a naturally-resolved harness-worktree host and clears harnessFlag', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '4', '--force-isolation', 'none'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
// routeDispatchIsolation's own stdout still reflects the FORCED value.
assert.equal(result.output.trim(), 'none');
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none');
assert.equal(sentinel.harness_flag, null);
} finally {
cleanup(dir);
}
});
test('an invalid --force-isolation value is ignored, not applied', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--force-isolation', 'bogus-mode'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(result.output.trim(), 'harness-worktree');
assert.equal(readSentinelRaw(dir).isolation, 'harness-worktree');
} finally {
cleanup(dir);
}
});
test('#3045 BLOCKER 1 — a later, plan-scoped call overwrites an earlier phase-only sentinel atomically', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
// Phase-level resolve (as the "Resolve ISOLATION" step performs it).
runGsdTools(['query', 'dispatch-isolation', '--raw', '--phase', '9'], dir, { GSD_RUNTIME: 'claude', HOME: dir });
assert.equal(readSentinelRaw(dir).plan, null);
// Per-plan gate degrades THIS plan to sequential (submodule intersection).
const r = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '9', '--plan', 'plan-sub', '--force-isolation', 'none'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(r.success, true, r.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none', 'the plan-scoped degrade must win over the stale phase-level record');
assert.equal(sentinel.plan, 'plan-sub');
assert.equal(sentinel.phase, '9');
} finally {
cleanup(dir);
}
});
test('the sentinel round-trips through the real reader (hooks/lib/isolation-sentinel.js)', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '2', '--plan', 'p1'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
const read = readSentinel(dir);
assert.equal(read.present, true);
assert.equal(read.stale, false);
assert.equal(read.malformed, false);
assert.equal(read.isolation, 'harness-worktree');
assert.equal(read.harnessFlag, 'isolation="worktree"');
assert.equal(read.phase, '2');
assert.equal(read.plan, 'p1');
} finally {
cleanup(dir);
}
});
});
describe('#3045 MAJOR — --harness-flag can now accept a bare CLI-flag value (Cursor real registry value + generalized parsing)', () => {
test('record-dispatch-isolation --harness-flag=--worktree persists the REAL cursor registry value verbatim', () => {
const cursorFlag = runtimes.cursor.runtime.harnessIsolationFlag;
assert.equal(cursorFlag, '--worktree', 'precondition: registry shape assumed by this test');
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', `--harness-flag=${cursorFlag}`, '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.harness_flag, cursorFlag);
} finally {
cleanup(dir);
}
});
test('record-dispatch-isolation --harness-flag=<bare-flag> persists ANY bare-CLI-flag-shaped value verbatim (parser is not Cursor-specific)', () => {
// A prior draft of this test asserted `runtimes.windsurf.runtime.harnessIsolationFlag
// === '--worktree'`, assuming Windsurf's registry entry mirrors Cursor's.
// It does not: Windsurf's `hostIntegration.dispatch.isolation` is 'none'
// and it declares NO `harnessIsolationFlag` at all — per ADR-1239
// (docs/adr/1239-gsd-embeddable-orchestration-engine.md:247,250),
// `pi`/`zcode`/`windsurf` "genuinely cannot benefit and correctly stay
// none" because they lack named/concurrent subagent dispatch, so there is
// no per-dispatch isolation flag for Windsurf to record. That was a wrong
// test expectation (a fabricated registry precondition), not a production
// defect — corrected here to prove the `--harness-flag=<value>` parser
// generalizes to any bare-CLI-flag-shaped value, not merely Cursor's
// specific '--worktree' string (which the sub-test above already pins).
assert.equal(
runtimes.windsurf.runtime.harnessIsolationFlag,
undefined,
'precondition: windsurf declares no harnessIsolationFlag (isolation: "none", ADR-1239)',
);
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', '--harness-flag=--isolated', '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(readSentinelRaw(dir).harness_flag, '--isolated');
} finally {
cleanup(dir);
}
});
test('the legacy space-separated form still rejects a value that looks like another flag (unchanged, regression pin)', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', '--harness-flag', '--worktree', '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(readSentinelRaw(dir).harness_flag, null, 'space form must not swallow a value shaped like a flag');
} finally {
cleanup(dir);
}
});
test('record-dispatch-isolation still errors with usage text when --isolation is missing', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(['query', 'record-dispatch-isolation'], dir, { HOME: dir });
assert.equal(result.success, false);
assert.match(result.error, /Usage: record-dispatch-isolation/);
} finally {
cleanup(dir);
}
});
test('record-dispatch-isolation accepts --plan and records it', () => {
const dir = createTempProject('gsd-3045-resolver-');
try {
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'none', '--phase', '5', '--plan', 'plan-x'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none');
assert.equal(sentinel.phase, '5');
assert.equal(sentinel.plan, 'plan-x');
} finally {
cleanup(dir);
}
});
});
describe('#3045 MINOR — writer/reader sentinel path derivation now agrees for a linked worktree without its own .planning/', () => {
function git(args, cwd) {
gitOrThrow(args, { cwd });
}
test('a sentinel written from a linked worktree (via --cwd) is found by readSentinel() called with that SAME worktree path', () => {
const mainRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3045-minor-main-'));
const wtParent = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3045-minor-wtparent-'));
try {
git(['init'], mainRepo);
git(['config', 'user.email', 'test@test.com'], mainRepo);
git(['config', 'user.name', 'Test'], mainRepo);
git(['config', 'commit.gpgsign', 'false'], mainRepo);
fs.writeFileSync(path.join(mainRepo, 'README.md'), 'placeholder\n');
git(['add', '-A'], mainRepo);
git(['commit', '-m', 'initial commit'], mainRepo);
// .planning/ is created AFTER the commit — uncommitted/untracked, the
// documented shape where a linked worktree does NOT get its own copy
// (git worktree only checks out tracked files).
fs.mkdirSync(path.join(mainRepo, '.planning'));
fs.writeFileSync(path.join(mainRepo, '.planning', 'config.json'), JSON.stringify({}));
const linked = path.join(wtParent, 'linked');
git(['worktree', 'add', linked, '-b', 'gsd-3045-minor-branch'], mainRepo);
assert.equal(fs.existsSync(path.join(linked, '.planning')), false, 'precondition: linked worktree has no own .planning/');
// Write FROM the linked worktree path — mirrors an orchestrator
// running in a linked worktree calling `dispatch-isolation`.
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--cwd', linked, '--phase', '1'],
mainRepo,
{ GSD_RUNTIME: 'claude', HOME: mainRepo },
);
assert.equal(result.success, true, result.error);
// The writer resolved up to the MAIN worktree (findProjectRoot(resolveMainWorktreeCwd(...))) —
// the sentinel must NOT exist at the linked worktree's own (nonexistent) .gsd/.
assert.equal(fs.existsSync(sentinelFile(linked)), false, 'writer must not have written under the linked worktree itself');
assert.equal(fs.existsSync(sentinelFile(mainRepo)), true, 'writer must have resolved up to the main worktree');
// The READER, given the raw linked-worktree cwd (exactly what a guard
// hook receives as data.cwd / workspace_roots[i]), must derive the SAME
// root the writer did and find the sentinel — this is the MINOR fix.
const read = readSentinel(linked);
assert.equal(read.present, true, 'reader must resolve the linked worktree up to the main worktree, same as the writer');
assert.equal(read.stale, false);
assert.equal(read.isolation, 'harness-worktree');
} finally {
cleanup(mainRepo);
cleanup(wtParent);
}
});
});

View File

@@ -47,12 +47,14 @@ const { describe, test, before, after } = 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 fc = require('./helpers/fast-check-setup.cjs');
const { runHook: runHookSeam } = require('./helpers/process-seam.cjs');
const { toLegacyResult } = require('./helpers/git-fixture.cjs');
const { toLegacyResult, gitOrThrow } = require('./helpers/git-fixture.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { SENTINEL_RELATIVE_PATH, SENTINEL_STALE_MS } = require('../hooks/lib/isolation-sentinel.js');
const { createTempDir, createTempProject, runGsdTools, cleanup } = require('./helpers.cjs');
const { SENTINEL_RELATIVE_PATH, SENTINEL_STALE_MS, readSentinel } = require('../hooks/lib/isolation-sentinel.js');
const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-agent-isolation-guard.js');
@@ -93,6 +95,20 @@ function runHook(payload, cwd, extraEnv = {}) {
return toLegacyResult(r);
}
/**
* #3045 follow-up (folded from tests/fix-3045-dispatch-isolation-resolver.test.cjs,
* #3333 wave 1): sentinel-file path/read helpers for the WRITE-side coverage
* below, which drives the real `gsd-tools.cjs query dispatch-isolation` CLI
* (routeDispatchIsolation) rather than the guard hook.
*/
function sentinelFile(dir) {
return path.join(dir, SENTINEL_RELATIVE_PATH);
}
function readSentinelRaw(dir) {
return JSON.parse(fs.readFileSync(sentinelFile(dir), 'utf-8'));
}
function agentPayload(overrides = {}) {
return {
hook_event_name: 'PreToolUse',
@@ -324,77 +340,59 @@ describe('gsd-agent-isolation-guard.js: #3045 BLOCKER regression — sentinel is
cleanup(useWorktreesFalseProject);
});
test('sentinel says isolation=none -> ALLOW even though registry resolves harness-worktree (the BLOCKER)', () => {
test('sentinel says isolation=none -> ALLOW even though registry resolves harness-worktree (the BLOCKER)', (t) => {
writeSentinel(harnessProject, { isolation: 'none' });
try {
const r = runHook(agentPayload(), harnessProject); // no isolation param on the dispatch
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(r.stdout, '');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(agentPayload(), harnessProject); // no isolation param on the dispatch
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(r.stdout, '');
});
test('sentinel says isolation=orchestrator-worktree -> ALLOW', () => {
test('sentinel says isolation=orchestrator-worktree -> ALLOW', (t) => {
writeSentinel(harnessProject, { isolation: 'orchestrator-worktree' });
try {
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(r.stdout, '');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(r.stdout, '');
});
test('sentinel says isolation=harness-worktree + dispatch missing the flag -> DENY', () => {
test('sentinel says isolation=harness-worktree + dispatch missing the flag -> DENY', (t) => {
writeSentinel(harnessProject, { isolation: 'harness-worktree', harnessFlag: 'isolation="worktree"' });
try {
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
});
test('sentinel says isolation=harness-worktree + dispatch carries the flag -> ALLOW', () => {
test('sentinel says isolation=harness-worktree + dispatch carries the flag -> ALLOW', (t) => {
writeSentinel(harnessProject, { isolation: 'harness-worktree', harnessFlag: 'isolation="worktree"' });
try {
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', isolation: 'worktree' } }),
harnessProject
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', isolation: 'worktree' } }),
harnessProject
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
});
test('STALE sentinel (older than SENTINEL_STALE_MS) is ignored -> falls back to registry (DENY, harness-worktree still applies)', () => {
test('STALE sentinel (older than SENTINEL_STALE_MS) is ignored -> falls back to registry (DENY, harness-worktree still applies)', (t) => {
// The stale sentinel LIES (says none) — proving the fallback re-derives
// from the registry instead of trusting it is exactly the point.
writeSentinel(harnessProject, { isolation: 'none', writtenAt: Date.now() - (SENTINEL_STALE_MS + 60000) });
try {
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
});
test('MALFORMED sentinel (invalid JSON) is treated as stale, never fatal -> falls back to registry (DENY)', () => {
test('MALFORMED sentinel (invalid JSON) is treated as stale, never fatal -> falls back to registry (DENY)', (t) => {
const sentinelPath = path.join(harnessProject, SENTINEL_RELATIVE_PATH);
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
fs.writeFileSync(sentinelPath, '{ this is not valid json');
try {
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
assert.equal(r.stderr.length > 0, true, 'must not crash — a clean block reason, not a stack trace');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(agentPayload(), harnessProject);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
assert.equal(r.stderr.length > 0, true, 'must not crash — a clean block reason, not a stack trace');
});
test('no sentinel + workflow.use_worktrees=false -> ALLOW (project-level opt-out, case (a) from the BLOCKER)', () => {
@@ -474,25 +472,22 @@ describe('gsd-agent-isolation-guard.js: #3045 BLOCKER 2 — default-install fail
cleanup(unconfiguredHarnessProject);
});
test('part A: fresh sentinel confirms harness-worktree but carries NO harness_flag, and the runtime is not confidently resolvable -> DENY (was ALLOW pre-fix)', () => {
test('part A: fresh sentinel confirms harness-worktree but carries NO harness_flag, and the runtime is not confidently resolvable -> DENY (was ALLOW pre-fix)', (t) => {
// This is the exact BLOCKER 2 regression: previously this branch fell
// through to the "not confident -> none" degrade and ALLOWED the
// dispatch to run unisolated, on the DEFAULT-INSTALL path (no `runtime`
// key in config.json — gsd-core/templates/config.json's shipped shape —
// and no ~/.gsd/defaults.json runtime either).
writeSentinel(unconfiguredHarnessProject, { isolation: 'harness-worktree', harnessFlag: null });
try {
const r = runHook(agentPayload(), unconfiguredHarnessProject, { HOME: unconfiguredHarnessProject });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — must DENY, not silently allow`);
const out = JSON.parse(r.stdout);
assert.equal(out.decision, 'block');
assert.match(out.reason, /cannot verify|harness_flag/i);
} finally {
cleanup(path.join(unconfiguredHarnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(unconfiguredHarnessProject, '.gsd')));
const r = runHook(agentPayload(), unconfiguredHarnessProject, { HOME: unconfiguredHarnessProject });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — must DENY, not silently allow`);
const out = JSON.parse(r.stdout);
assert.equal(out.decision, 'block');
assert.match(out.reason, /cannot verify|harness_flag/i);
});
test('part B: ~/.gsd/defaults.json runtime (installer-persisted, #2395) is now a confident signal — makes the default install enforce', () => {
test('part B: ~/.gsd/defaults.json runtime (installer-persisted, #2395) is now a confident signal — makes the default install enforce', (t) => {
// No sentinel at all here — pure conservative-fallback path. Before this
// fix, an unconfigured project (no config.json runtime key, the COMMON
// scaffold shape) always fell back to 'none'/allow regardless of what the
@@ -501,32 +496,26 @@ describe('gsd-agent-isolation-guard.js: #3045 BLOCKER 2 — default-install fail
// already writes for every non-Claude install) is read as confidently as
// GSD_RUNTIME or config.json's own key.
const home = mkProject('gsd-aig-b2-home-');
try {
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: 'claude' }));
t.after(() => cleanup(home));
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: 'claude' }));
const r = runHook(agentPayload(), unconfiguredHarnessProject, { HOME: home });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — defaults.json runtime must be enforced`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
} finally {
cleanup(home);
}
const r = runHook(agentPayload(), unconfiguredHarnessProject, { HOME: home });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — defaults.json runtime must be enforced`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
});
test('part B (negative control): a project WITH its own config.json runtime key still wins over defaults.json', () => {
test('part B (negative control): a project WITH its own config.json runtime key still wins over defaults.json', (t) => {
const home = mkProject('gsd-aig-b2-home2-');
try {
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
// defaults.json says a runtime with NO harness-worktree capability;
// config.json's own `runtime: claude` must take precedence.
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: 'windsurf' }));
t.after(() => cleanup(home));
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
// defaults.json says a runtime with NO harness-worktree capability;
// config.json's own `runtime: claude` must take precedence.
fs.writeFileSync(path.join(home, '.gsd', 'defaults.json'), JSON.stringify({ runtime: 'windsurf' }));
const r = runHook(agentPayload(), harnessProject, { HOME: home });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
} finally {
cleanup(home);
}
const r = runHook(agentPayload(), harnessProject, { HOME: home });
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr}`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
});
});
@@ -542,47 +531,38 @@ describe('gsd-agent-isolation-guard.js: #3045 SECURITY F2 — sentinel bound to
cleanup(harnessProject);
});
test('a fresh "none" sentinel for a DIFFERENT phase than this dispatch is not applied — falls through to conservative fallback and DENIES', () => {
test('a fresh "none" sentinel for a DIFFERENT phase than this dispatch is not applied — falls through to conservative fallback and DENIES', (t) => {
// Sentinel legitimately recorded 'none' for phase 1 (e.g. a submodule
// degrade). This dispatch's own description names phase 2 — the guard
// must not reuse phase 1's stale-but-fresh "none" to authorize it.
writeSentinel(harnessProject, { isolation: 'none', phase: '1', plan: 'plan-a' });
try {
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'Execute plan plan-b of phase 2' } }),
harnessProject,
);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — mismatched sentinel must not silently allow`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'Execute plan plan-b of phase 2' } }),
harnessProject,
);
assert.equal(r.status, 2, `stdout: ${r.stdout} stderr: ${r.stderr} — mismatched sentinel must not silently allow`);
assert.equal(JSON.parse(r.stdout).decision, 'block');
});
test('a fresh sentinel for the SAME phase/plan as this dispatch is applied normally (positive control)', () => {
test('a fresh sentinel for the SAME phase/plan as this dispatch is applied normally (positive control)', (t) => {
writeSentinel(harnessProject, { isolation: 'none', phase: '2', plan: 'plan-b' });
try {
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'Execute plan plan-b of phase 2' } }),
harnessProject,
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'Execute plan plan-b of phase 2' } }),
harnessProject,
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
});
test('a dispatch whose description does not match the expected shape does not itself trigger a mismatch (best-effort extraction)', () => {
test('a dispatch whose description does not match the expected shape does not itself trigger a mismatch (best-effort extraction)', (t) => {
writeSentinel(harnessProject, { isolation: 'none', phase: '2', plan: 'plan-b' });
try {
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'some other free-form text' } }),
harnessProject,
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const r = runHook(
agentPayload({ tool_input: { subagent_type: 'gsd-executor', description: 'some other free-form text' } }),
harnessProject,
);
assert.equal(r.status, 0, `stdout: ${r.stdout} stderr: ${r.stderr}`);
});
});
@@ -613,39 +593,284 @@ describe('gsd-agent-isolation-guard.js: #3045 MAJOR — clock seam boundary cove
return { now: () => nowMs };
}
test('sentinel exactly at SENTINEL_STALE_MS - 1 is still FRESH (trusted)', () => {
test('sentinel exactly at SENTINEL_STALE_MS - 1 is still FRESH (trusted)', (t) => {
const writtenAt = 1_000_000;
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
try {
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS - 1) });
assert.equal(state.isolation, 'none', 'still within the trust window — must use the sentinel, not the registry fallback');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS - 1) });
assert.equal(state.isolation, 'none', 'still within the trust window — must use the sentinel, not the registry fallback');
});
test('sentinel exactly AT SENTINEL_STALE_MS is STALE (age > threshold is the only fresh condition)', () => {
test('sentinel exactly AT SENTINEL_STALE_MS is STALE (age > threshold is the only fresh condition)', (t) => {
const writtenAt = 1_000_000;
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
try {
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS) });
// Registry fallback for this project resolves harness-worktree (claude,
// no workflow.use_worktrees:false) — proves the sentinel's 'none' was
// NOT trusted at exactly the boundary.
assert.equal(state.isolation, 'harness-worktree');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS) });
// Registry fallback for this project resolves harness-worktree (claude,
// no workflow.use_worktrees:false) — proves the sentinel's 'none' was
// NOT trusted at exactly the boundary.
assert.equal(state.isolation, 'harness-worktree');
});
test('sentinel at SENTINEL_STALE_MS + 1 is STALE', () => {
test('sentinel at SENTINEL_STALE_MS + 1 is STALE', (t) => {
const writtenAt = 1_000_000;
writeSentinel(harnessProject, { isolation: 'none', writtenAt });
try {
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS + 1) });
assert.equal(state.isolation, 'harness-worktree');
} finally {
cleanup(path.join(harnessProject, '.gsd'));
}
t.after(() => cleanup(path.join(harnessProject, '.gsd')));
const state = guardModule.resolveIsolationState(harnessProject, { clock: fixedClock(writtenAt + SENTINEL_STALE_MS + 1) });
assert.equal(state.isolation, 'harness-worktree');
});
});
// Folded from tests/fix-3045-dispatch-isolation-resolver.test.cjs (#3333 wave
// 1, test-only consolidation — no behavior change). These describe blocks
// cover the sentinel WRITE side: `gsd-tools.cjs query dispatch-isolation`
// (routeDispatchIsolation) persists the resolved isolation decision as an
// unconditional side effect of resolving it, and `record-dispatch-isolation`
// (routeRecordDispatchIsolation) is the explicit fallback/testable primitive
// sharing the same atomic-write implementation. Every test here drives the
// REAL gsd-tools.cjs CLI (via runGsdTools) and asserts on the sentinel file
// it actually wrote, parsed as JSON.
describe('#3045 CORE REDESIGN — dispatch-isolation records as an unconditional side effect', () => {
test('a plain --raw query with no explicit isolation-record verb still writes the sentinel', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
assert.equal(fs.existsSync(sentinelFile(dir)), false, 'precondition: no sentinel yet');
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '7'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(result.output.trim(), 'harness-worktree');
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'harness-worktree');
assert.equal(sentinel.harness_flag, 'isolation="worktree"');
assert.equal(sentinel.phase, '7');
assert.equal(sentinel.plan, null);
assert.equal(typeof sentinel.written_at, 'number');
});
test('--json output and the recorded sentinel agree on isolation + harnessFlag', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'dispatch-isolation', '--json', '--phase', '3', '--plan', 'plan-b'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
const parsed = JSON.parse(result.output);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, parsed.isolation);
assert.equal(sentinel.harness_flag, parsed.harnessFlag);
assert.equal(sentinel.phase, '3');
assert.equal(sentinel.plan, 'plan-b');
});
test('--force-isolation none overrides a naturally-resolved harness-worktree host and clears harnessFlag', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '4', '--force-isolation', 'none'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
// routeDispatchIsolation's own stdout still reflects the FORCED value.
assert.equal(result.output.trim(), 'none');
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none');
assert.equal(sentinel.harness_flag, null);
});
test('an invalid --force-isolation value is ignored, not applied', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--force-isolation', 'bogus-mode'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(result.output.trim(), 'harness-worktree');
assert.equal(readSentinelRaw(dir).isolation, 'harness-worktree');
});
test('#3045 BLOCKER 1 — a later, plan-scoped call overwrites an earlier phase-only sentinel atomically', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
// Phase-level resolve (as the "Resolve ISOLATION" step performs it).
runGsdTools(['query', 'dispatch-isolation', '--raw', '--phase', '9'], dir, { GSD_RUNTIME: 'claude', HOME: dir });
assert.equal(readSentinelRaw(dir).plan, null);
// Per-plan gate degrades THIS plan to sequential (submodule intersection).
const r = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '9', '--plan', 'plan-sub', '--force-isolation', 'none'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
assert.equal(r.success, true, r.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none', 'the plan-scoped degrade must win over the stale phase-level record');
assert.equal(sentinel.plan, 'plan-sub');
assert.equal(sentinel.phase, '9');
});
test('the sentinel round-trips through the real reader (hooks/lib/isolation-sentinel.js)', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
runGsdTools(
['query', 'dispatch-isolation', '--raw', '--phase', '2', '--plan', 'p1'],
dir,
{ GSD_RUNTIME: 'claude', HOME: dir },
);
const read = readSentinel(dir);
assert.equal(read.present, true);
assert.equal(read.stale, false);
assert.equal(read.malformed, false);
assert.equal(read.isolation, 'harness-worktree');
assert.equal(read.harnessFlag, 'isolation="worktree"');
assert.equal(read.phase, '2');
assert.equal(read.plan, 'p1');
});
});
describe('#3045 MAJOR — --harness-flag can now accept a bare CLI-flag value (Cursor real registry value + generalized parsing)', () => {
test('record-dispatch-isolation --harness-flag=--worktree persists the REAL cursor registry value verbatim', (t) => {
const cursorFlag = runtimes.cursor.runtime.harnessIsolationFlag;
assert.equal(cursorFlag, '--worktree', 'precondition: registry shape assumed by this test');
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', `--harness-flag=${cursorFlag}`, '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.harness_flag, cursorFlag);
});
test('record-dispatch-isolation --harness-flag=<bare-flag> persists ANY bare-CLI-flag-shaped value verbatim (parser is not Cursor-specific)', (t) => {
// A prior draft of this test asserted `runtimes.windsurf.runtime.harnessIsolationFlag
// === '--worktree'`, assuming Windsurf's registry entry mirrors Cursor's.
// It does not: Windsurf's `hostIntegration.dispatch.isolation` is 'none'
// and it declares NO `harnessIsolationFlag` at all — per ADR-1239
// (docs/adr/1239-gsd-embeddable-orchestration-engine.md:247,250),
// `pi`/`zcode`/`windsurf` "genuinely cannot benefit and correctly stay
// none" because they lack named/concurrent subagent dispatch, so there is
// no per-dispatch isolation flag for Windsurf to record. That was a wrong
// test expectation (a fabricated registry precondition), not a production
// defect — corrected here to prove the `--harness-flag=<value>` parser
// generalizes to any bare-CLI-flag-shaped value, not merely Cursor's
// specific '--worktree' string (which the sub-test above already pins).
assert.equal(
runtimes.windsurf.runtime.harnessIsolationFlag,
undefined,
'precondition: windsurf declares no harnessIsolationFlag (isolation: "none", ADR-1239)',
);
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', '--harness-flag=--isolated', '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(readSentinelRaw(dir).harness_flag, '--isolated');
});
test('the legacy space-separated form still rejects a value that looks like another flag (unchanged, regression pin)', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'harness-worktree', '--harness-flag', '--worktree', '--phase', '1'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
assert.equal(readSentinelRaw(dir).harness_flag, null, 'space form must not swallow a value shaped like a flag');
});
test('record-dispatch-isolation still errors with usage text when --isolation is missing', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(['query', 'record-dispatch-isolation'], dir, { HOME: dir });
assert.equal(result.success, false);
assert.match(result.error, /Usage: record-dispatch-isolation/);
});
test('record-dispatch-isolation accepts --plan and records it', (t) => {
const dir = createTempProject('gsd-3045-resolver-');
t.after(() => cleanup(dir));
const result = runGsdTools(
['query', 'record-dispatch-isolation', '--isolation', 'none', '--phase', '5', '--plan', 'plan-x'],
dir,
{ HOME: dir },
);
assert.equal(result.success, true, result.error);
const sentinel = readSentinelRaw(dir);
assert.equal(sentinel.isolation, 'none');
assert.equal(sentinel.phase, '5');
assert.equal(sentinel.plan, 'plan-x');
});
});
describe('#3045 MINOR — writer/reader sentinel path derivation now agrees for a linked worktree without its own .planning/', () => {
function git(args, cwd) {
gitOrThrow(args, { cwd });
}
test('a sentinel written from a linked worktree (via --cwd) is found by readSentinel() called with that SAME worktree path', (t) => {
const mainRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3045-minor-main-'));
const wtParent = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3045-minor-wtparent-'));
t.after(() => {
cleanup(mainRepo);
cleanup(wtParent);
});
git(['init'], mainRepo);
git(['config', 'user.email', 'test@test.com'], mainRepo);
git(['config', 'user.name', 'Test'], mainRepo);
git(['config', 'commit.gpgsign', 'false'], mainRepo);
fs.writeFileSync(path.join(mainRepo, 'README.md'), 'placeholder\n');
git(['add', '-A'], mainRepo);
git(['commit', '-m', 'initial commit'], mainRepo);
// .planning/ is created AFTER the commit — uncommitted/untracked, the
// documented shape where a linked worktree does NOT get its own copy
// (git worktree only checks out tracked files).
fs.mkdirSync(path.join(mainRepo, '.planning'));
fs.writeFileSync(path.join(mainRepo, '.planning', 'config.json'), JSON.stringify({}));
const linked = path.join(wtParent, 'linked');
git(['worktree', 'add', linked, '-b', 'gsd-3045-minor-branch'], mainRepo);
assert.equal(fs.existsSync(path.join(linked, '.planning')), false, 'precondition: linked worktree has no own .planning/');
// Write FROM the linked worktree path — mirrors an orchestrator
// running in a linked worktree calling `dispatch-isolation`.
const result = runGsdTools(
['query', 'dispatch-isolation', '--raw', '--cwd', linked, '--phase', '1'],
mainRepo,
{ GSD_RUNTIME: 'claude', HOME: mainRepo },
);
assert.equal(result.success, true, result.error);
// The writer resolved up to the MAIN worktree (findProjectRoot(resolveMainWorktreeCwd(...))) —
// the sentinel must NOT exist at the linked worktree's own (nonexistent) .gsd/.
assert.equal(fs.existsSync(sentinelFile(linked)), false, 'writer must not have written under the linked worktree itself');
assert.equal(fs.existsSync(sentinelFile(mainRepo)), true, 'writer must have resolved up to the main worktree');
// The READER, given the raw linked-worktree cwd (exactly what a guard
// hook receives as data.cwd / workspace_roots[i]), must derive the SAME
// root the writer did and find the sentinel — this is the MINOR fix.
const read = readSentinel(linked);
assert.equal(read.present, true, 'reader must resolve the linked worktree up to the main worktree, same as the writer');
assert.equal(read.stale, false);
assert.equal(read.isolation, 'harness-worktree');
});
});

View File

@@ -21,10 +21,23 @@ const fs = require('fs');
const {
convertClaudeCommandToClaudeSkill,
convertClaudeToHermesMarkdown,
projectNamedDispatchToStructuralDelegate,
_hostIntegrationDispatch,
_resolveAvailableGsdRoles,
HERMES_DISPATCH_TOOL_CONFIG,
maskStringLiterals,
findDispatchCallSpans,
_assertProjectionComplete,
applyClaudeCodeBrandSwap,
convertClaudeToWindsurfMarkdown,
install,
uninstall,
} = require('../bin/install.js');
const { installRuntimeArtifacts } = require('../gsd-core/bin/lib/install-engine.cjs');
const { parseFrontmatter, cleanup } = require('./helpers.cjs');
const { nestedSkillPath } = require('./helpers/nested-layout.cjs');
const pkg = require('../package.json');
const {
@@ -35,6 +48,37 @@ const {
const manifest = loadSkillsManifest();
const resolvedProfileFull = resolveProfile({ modes: [], manifest });
// ─── #2284 shared fixtures/helpers (delegate_task dispatch projection) ──────
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);
}
// ─── convertClaudeCommandToClaudeSkill (used by Hermes via copyCommandsAsClaudeSkills) ──
describe('Hermes Agent: convertClaudeCommandToClaudeSkill', () => {
@@ -395,3 +439,835 @@ describe('#1383 regression: gsd-tools version lookup without a runtime-root pack
assert.strictEqual(v, '', 'garbled VERSION is rejected, so the caller omits the field');
});
});
// ─── #2284: Hermes named-dispatch → delegate_task projection ────────────────
// Folded from tests/fix-2284-hermes-agent-delegate-task-projection.test.cjs
// (test-hygiene consolidation, issue #3333). 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.
// ─── 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: [\x00] 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('[\x00]'), 'NUL byte preserved verbatim');
assert.strictEqual(out.split('\x00').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

@@ -10,8 +10,11 @@
* sentinels fail-closed in negotiation (safe documented default, never propagate).
*/
process.env.GSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const {
@@ -26,6 +29,38 @@ const {
validateCapability,
} = require(path.join(__dirname, '../gsd-core/bin/lib/capability-validator.cjs'));
// Folded from tests/fix-2598-opencode-background-dispatch.test.cjs and
// tests/fix-2603-kimi-code-host-matrix.test.cjs (#3333, test-hygiene backlog).
const ROOT = path.join(__dirname, '..');
const OPENCODE_DESCRIPTOR = path.join(ROOT, 'capabilities', 'opencode', 'capability.json');
const KIMI_CODE_DESCRIPTOR = path.join(ROOT, 'capabilities', 'kimi-code', 'capability.json');
const HOST_INTEGRATION_MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
function opencodeDispatch() {
const parsed = JSON.parse(fs.readFileSync(OPENCODE_DESCRIPTOR, 'utf8'));
return parsed.runtime.hostIntegration.dispatch;
}
function kimiCodeAxes() {
return JSON.parse(fs.readFileSync(KIMI_CODE_DESCRIPTOR, 'utf8')).runtime.hostIntegration;
}
/** Extract the `## <host>` section body, stopping at the next top-level host heading. */
function matrixSection(host) {
const matrix = fs.readFileSync(HOST_INTEGRATION_MATRIX, 'utf8');
const start = matrix.indexOf(`\n## ${host}\n`);
if (start === -1) return null;
const rest = matrix.slice(start + 1);
const end = rest.indexOf('\n## ');
return end === -1 ? rest : rest.slice(0, end);
}
/** Read the `| <axis> | <value> | …` cell out of a matrix section. */
function matrixValue(section, axis) {
const row = section.split('\n').find((l) => l.startsWith(`| ${axis} |`));
return row ? row.split('|')[2].trim() : null;
}
// All 8 scalar hostIntegration axis keys
const SCALAR_AXES = ['embeddingMode', 'commandSurface', 'modelMode', 'hookBus', 'stateIO', 'transport', 'runtime'];
// All 6 dispatch sub-keys (includes backgroundDispatch added in feat/1679-dispatch-flatten)
@@ -452,3 +487,205 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
});
});
});
// allow-test-rule: source-text-is-the-product #2598 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
describe('#2598: OpenCode does not declare background/concurrent subagent dispatch', () => {
test('descriptor declares background: false', () => {
assert.equal(
opencodeDispatch().background,
false,
'OpenCode subagent dispatch is synchronous unless an experimental opt-in flag is set',
);
});
test('descriptor declares backgroundDispatch: false', () => {
assert.equal(
opencodeDispatch().backgroundDispatch,
false,
'concurrent dispatch requires an opt-in flag, so it must not be declared as available',
);
});
test('the capabilities that ARE real are left intact', () => {
// Narrow the blast radius: this fix must not quietly downgrade neighbouring
// sub-fields that were never in question.
const d = opencodeDispatch();
assert.equal(d.namedDispatch, true, 'named subagent dispatch is genuinely supported');
assert.equal(d.subagentToolkit, 'full', 'the general subagent has full tool access');
assert.equal(d.isolation, 'orchestrator-worktree',
'isolation is orchestrator-managed via `opencode run --dir`, unaffected by #2598');
});
test('the host-integration matrix agrees with the descriptor', () => {
// ADR-1239 designates the matrix the deployment source-of-truth; a
// descriptor/matrix disagreement is how this defect survived in the first
// place (the matrix said true, the ADR binding table said false).
const matrix = fs.readFileSync(HOST_INTEGRATION_MATRIX, 'utf8');
const section = matrix.slice(matrix.indexOf('## opencode'));
const end = section.indexOf('\n## ');
const opencodeSection = end === -1 ? section : section.slice(0, end);
for (const field of ['dispatch.background', 'dispatch.backgroundDispatch']) {
const row = opencodeSection.split('\n').find((l) => l.startsWith(`| ${field} |`));
assert.ok(row, `matrix must document ${field} for opencode`);
const value = row.split('|')[2].trim();
assert.equal(value, 'false', `matrix ${field} must match the descriptor`);
}
});
});
// allow-test-rule: source-text-is-the-product #2603 — the descriptor JSON and the
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
describe('#2603: the host-integration matrix documents kimi-code', () => {
test('a `## kimi-code` section exists', () => {
assert.ok(
matrixSection('kimi-code'),
'the matrix is the deployment source-of-truth for every installed runtime; kimi-code must have a section',
);
});
test('every hostIntegration axis kimi-code declares is documented in the matrix', () => {
const section = matrixSection('kimi-code');
const axes = kimiCodeAxes();
const scalarAxes = Object.keys(axes).filter((k) => k !== 'dispatch');
for (const axis of scalarAxes) {
assert.ok(
matrixValue(section, axis),
`matrix must document the "${axis}" axis for kimi-code`,
);
}
// `builtInSubagents` is a GSD-side list, not a negotiated axis — the matrix
// documents it in prose, not as its own row.
const dispatchAxes = Object.keys(axes.dispatch).filter((k) => k !== 'builtInSubagents');
for (const axis of dispatchAxes) {
assert.ok(
matrixValue(section, `dispatch.${axis}`),
`matrix must document the "dispatch.${axis}" sub-axis for kimi-code`,
);
}
});
test('the matrix values agree with the shipped descriptor', () => {
const section = matrixSection('kimi-code');
const axes = kimiCodeAxes();
for (const axis of Object.keys(axes).filter((k) => k !== 'dispatch')) {
assert.equal(
matrixValue(section, axis),
String(axes[axis]),
`matrix "${axis}" must match the descriptor`,
);
}
for (const axis of Object.keys(axes.dispatch).filter((k) => k !== 'builtInSubagents')) {
assert.equal(
matrixValue(section, `dispatch.${axis}`),
String(axes.dispatch[axis]),
`matrix "dispatch.${axis}" must match the descriptor`,
);
}
});
test('the kimi-code section is sourced independently of the kimi section', () => {
// The two are distinct products (Python kimi-cli vs TypeScript Kimi Code CLI);
// the issue's central requirement is that kimi's section was NOT copied. The
// check is scoped to the axis ROWS — the section's prose intro deliberately
// names kimi's Python API to draw the contrast, which is the opposite of a copy.
const rows = matrixSection('kimi-code')
.split('\n')
.filter((l) => l.startsWith('| ') && !l.startsWith('| Axis |') && !l.startsWith('|---'));
assert.ok(rows.length >= 11, 'expected a row per hostIntegration axis');
for (const row of rows) {
assert.ok(
!row.includes('kimi_cli'),
`kimi-code axis row must not cite the Python kimi-cli: ${row.slice(0, 60)}`,
);
assert.ok(
!row.includes('moonshotai.github.io/kimi-cli'),
`kimi-code axis row must not cite kimi-cli docs: ${row.slice(0, 60)}`,
);
}
assert.ok(
rows.some((r) => r.includes('kimi-code/blob/main/docs')),
'kimi-code axes must cite the Kimi Code CLI docs',
);
});
});
describe('#2603: axis values inherited from the Python kimi descriptor are corrected', () => {
test('embeddingMode is declarative — plugins expose no in-process API', () => {
assert.equal(kimiCodeAxes().embeddingMode, 'declarative');
});
test('kimi-code therefore classifies as the declarative-cli profile', () => {
assert.equal(profileOf(kimiCodeAxes()), 'declarative-cli');
});
test('dispatch.nested is true — the coder built-in dispatches nested sub-agents', () => {
assert.equal(kimiCodeAxes().dispatch.nested, true);
});
test('dispatch.maxDepth is the undocumented sentinel, not a guessed integer', () => {
assert.equal(kimiCodeAxes().dispatch.maxDepth, 'undocumented');
});
test('namedDispatch stays false — GSD installs no agent files for this host', () => {
// Guard against a well-meaning "the docs say custom agents exist" edit: flipping
// this makes resolveDispatchType return `gsd-planner` unchanged, which kimi-code
// cannot dispatch (docs/migration/kimi-to-kimi-code.md).
assert.equal(kimiCodeAxes().dispatch.namedDispatch, false);
});
test('the undocumented maxDepth sentinel is reported as a sentinel, not as malformed', () => {
// Surfaced by this change: maxDepth was the ONE dispatch sub-axis with no
// sentinel-specific warning, so the documented fail-closed value was reported
// as "missing or not a number" — indistinguishable from a genuinely broken
// descriptor. kimi-code would have been the sixth runtime to hit that path.
const { warnings } = negotiateHostCapabilities(kimiCodeAxes());
assert.ok(
warnings.some((w) => w.includes('dispatch.maxDepth is undocumented')),
`expected a maxDepth sentinel warning, got: ${JSON.stringify(warnings)}`,
);
assert.ok(
!warnings.some((w) => w.includes('maxDepth is missing or not a number')),
'the documented sentinel must not be reported as a malformed value',
);
});
test('a genuinely malformed maxDepth is still reported as malformed', () => {
// Boundary: the sentinel carve-out must not swallow the real error case.
const axes = kimiCodeAxes();
const malformed = { ...axes, dispatch: { ...axes.dispatch, maxDepth: 'not-a-number' } };
const { warnings } = negotiateHostCapabilities(malformed);
assert.ok(
warnings.some((w) => w.includes('maxDepth is missing or not a number')),
`expected the malformed-value warning, got: ${JSON.stringify(warnings)}`,
);
});
test('both maxDepth paths still degrade the effective value closed to 0', () => {
const axes = kimiCodeAxes();
assert.equal(negotiateHostCapabilities(axes).effective.dispatch.maxDepth, 0);
const malformed = { ...axes, dispatch: { ...axes.dispatch, maxDepth: 'not-a-number' } };
assert.equal(negotiateHostCapabilities(malformed).effective.dispatch.maxDepth, 0);
});
test('the axes that were already correct are left intact', () => {
const axes = kimiCodeAxes();
assert.equal(axes.commandSurface, 'slash-file');
assert.equal(axes.modelMode, 'passive');
assert.equal(axes.hookBus, 'host');
assert.equal(axes.stateIO, 'filesystem');
assert.equal(axes.transport, 'mcp');
assert.equal(axes.runtime, 'node');
assert.equal(axes.dispatch.background, true);
assert.equal(axes.dispatch.backgroundDispatch, true);
assert.equal(axes.dispatch.subagentToolkit, 'built-in-only');
assert.equal(axes.dispatch.isolation, 'orchestrator-worktree');
});
});

View File

@@ -38,7 +38,7 @@ try {
else process.env.GSD_TEST_MODE = savedTestMode;
}
const { install, mergeClaudePermissions, GSD_CLAUDE_ALLOW_PERMISSIONS, GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS, GSD_CLAUDE_DENY_PERMISSIONS, rewriteLegacyManagedNodeHookCommands, resolveNodeRunner } = installExports || {};
const { install, mergeClaudePermissions, GSD_CLAUDE_ALLOW_PERMISSIONS, GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS, GSD_CLAUDE_DENY_PERMISSIONS, rewriteLegacyManagedNodeHookCommands, resolveNodeRunner, copyWithPathReplacement } = installExports || {};
const {
installRuntimeArtifacts,
@@ -1357,3 +1357,72 @@ describe('#1924: preserveUserArtifacts helper exists in install.js', () => {
});
});
}
// ─── #3333 — copyWithPathReplacement TOCTOU: source vanishes mid-copy ────────
//
// copyWithPathReplacement enumerates srcDir via readdirSync, then later reads
// (or copies) each listed entry. A filesystem is not transactional: the file
// readdirSync named can be deleted by a concurrent process before the loop
// reaches it, throwing an unhandled ENOENT and crashing the whole install.
// This is not hypothetical — tests/planning-prompt-drift.test.cjs writes a
// throwaway fixture directly into the real, shared gsd-core/workflows/ and
// deletes it in t.after(); a concurrently-running install path that lists
// that directory can observe the fixture in its readdirSync snapshot but hit
// ENOENT reading it once the writer's cleanup fires. Confirmed crash from a
// real remote gsd-test run:
// Error: ENOENT: no such file or directory, open
// '/work/gsd-core/workflows/zzz-e5-drift-fixture.md'
// at Object.readFileSync (node:fs:441:20)
// at copyWithPathReplacement (/work/bin/install.js:7888:24)
describe('#3333 regression: copyWithPathReplacement tolerates a source file vanishing mid-copy (TOCTOU)', () => {
test('a .md file deleted between readdirSync and read is skipped, siblings still copied, no crash', (t) => {
assert.strictEqual(typeof copyWithPathReplacement, 'function',
'copyWithPathReplacement must be exported from bin/install.js');
const srcDir = createTempDir('gsd-3333-src-');
const destRoot = createTempDir('gsd-3333-dest-');
t.after(() => {
cleanup(srcDir);
cleanup(destRoot);
});
fs.writeFileSync(path.join(srcDir, 'alpha.md'), '# alpha\n');
fs.writeFileSync(path.join(srcDir, 'vanish.md'), '# vanish\n');
fs.writeFileSync(path.join(srcDir, 'beta.md'), '# beta\n');
const vanishPath = path.join(srcDir, 'vanish.md');
// Deterministically inject the race at its true origin point: readdirSync
// (called inside copyWithPathReplacement) returns the snapshot that still
// includes 'vanish.md', but the file is removed from disk immediately
// after that snapshot is taken and before the entry loop reaches it — the
// same shape as a concurrent test suite's t.after() cleanup firing mid-copy.
const origReaddirSync = fs.readdirSync;
fs.readdirSync = function (dir, opts) {
const result = origReaddirSync.call(fs, dir, opts);
if (dir === srcDir) {
try { fs.unlinkSync(vanishPath); } catch { /* already gone */ }
}
return result;
};
t.after(() => { fs.readdirSync = origReaddirSync; });
const destDir = path.join(destRoot, 'out');
assert.doesNotThrow(() => {
copyWithPathReplacement(srcDir, destDir, '~/.claude/', 'claude', false, false, destRoot);
}, 'copyWithPathReplacement must not throw when a listed source file vanishes before it is read (#3333)');
assert.ok(fs.existsSync(path.join(destDir, 'alpha.md')),
'alpha.md (unaffected sibling before the vanished entry) must still be copied');
assert.strictEqual(fs.readFileSync(path.join(destDir, 'alpha.md'), 'utf8'), '# alpha\n');
assert.ok(fs.existsSync(path.join(destDir, 'beta.md')),
'beta.md (unaffected sibling after the vanished entry) must still be copied');
assert.strictEqual(fs.readFileSync(path.join(destDir, 'beta.md'), 'utf8'), '# beta\n');
assert.ok(!fs.existsSync(path.join(destDir, 'vanish.md')),
'vanish.md destination must not exist — the vanished source must be skipped, not partially written');
});
});

View File

@@ -23,10 +23,13 @@
* tests/trae-upgrades.test.cjs.
*/
const { test } = require('node:test');
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
process.env['GSD_TEST_MODE'] = '1';
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const {
@@ -36,6 +39,12 @@ const {
PROFILE_BASELINES,
UNDOCUMENTED,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const { getProjectInstructionFile } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
const { convertClaudeToTraeMarkdown } = require('../bin/install.js');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
const { runMinimalInstall, walk } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const TRAE_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'trae', 'capability.json'), 'utf8'),
@@ -155,3 +164,255 @@ test('no `runtime === "trae"` string-equality branch remains in the descriptor-m
`AC2: no hardcoded runtime==='trae' branch may remain in ${path.relative(repoRoot, file)}; found: ${offenders.join(', ')}`);
}
});
// -- folded from tests/fix-2658-trae-runtime-detection-and-instruction-path.test.cjs (#3333) --
/**
* Regression tests for #2658 — Trae runtime not detected in workflow
* runtime-detection blocks (falls back to claude), and the install-time
* `CLAUDE.md` path rewrite mutilates the claude fallback into a malformed
* path instead of resolving to the Trae rules file.
*
* Defects collided (see
* .gsd/bug/fix-2658-trae-runtime-not-detected-falls-back-to-/10-diagnosis.md):
*
* 1. `gsd-core/workflows/new-project.md` AND `gsd-core/workflows/ingest-docs.md`
* (found during this remediation — same pattern, same gap, not just
* new-project.md as originally reported) never recognized trae (path
* `/.trae/` or env `TRAE_CONFIG_DIR`) in their runtime-detection blocks —
* fell through to `RUNTIME=claude`.
* 2. The `trae.js` entry in `bin/install.js`'s `RUNTIME_CONTENT_DISPATCH`
* replaced bare `CLAUDE.md` first, leaving a stale `.claude/` prefix:
* `.claude/CLAUDE.md` -> `.claude/.trae/rules/`.
* 3. `convertClaudeToTraeMarkdown` (mirrored in `bin/install.js` and
* `src/runtime-artifact-conversion.cts`) had the same class of bug but a
* DIFFERENT wrong output (`.trae/.trae/rules/`), because its generic
* `.claude/` -> `.trae/` rewrite ran after the bare `CLAUDE.md` rewrite
* and re-mutated the leftover prefix.
* 4. `capabilities/trae/capability.json` didn't declare
* `hostBehaviors.projectInstructionFile`, so even a correctly-detected
* trae runtime resolved to the generic `AGENTS.md` default via
* `getProjectInstructionFile`.
* 5. Found by the end-to-end install test below, one level deeper than the
* static trace: `copyWithPathReplacement` (bin/install.js) runs a
* GENERIC `~/.claude/` / `$HOME/.claude/` / `./.claude/` -> runtime-dir
* rewrite on every .md file BEFORE calling `convertClaudeToTraeMarkdown`,
* substituting a `pathPrefix` the converter is never given (it differs
* per install: relative for a project-local install, an arbitrary
* absolute path for a local install rooted elsewhere, `~/.trae/` for a
* global one). The converter's `.claude/CLAUDE.md`-specific patterns
* (defect 3's fix) never fire on that already-rewritten text, and the
* bare fallback still doubles the prefix — a first attempt at fixing
* this handled only the `./.trae/CLAUDE.md` shape and missed the
* `~/.claude/` / `$HOME/.claude/` forms `gsd-core/workflows/profile-user.md`
* actually uses, caught by row 12 (the real spawned install) below on a
* second run. Fixed with a prefix-preserving pattern (capture whatever
* precedes a `.trae/` tail, keep it, fix only the filename suffix)
* instead of assuming one fixed shape.
*/
describe('#2658: trae runtime detection and instruction path (folded from fix-2658-trae-runtime-detection-and-instruction-path.test.cjs)', () => {
const REPO_ROOT = path.join(__dirname, '..');
const MALFORMED_SINGLE = '.claude/.trae/rules';
const MALFORMED_DOUBLE = '.trae/.trae/rules';
const EXPECTED_PATH = '.trae/rules/rules.md';
describe('#2658 acceptance criterion 2: getProjectInstructionFile resolves trae to a concrete file', () => {
test('trae maps to .trae/rules/rules.md (not the generic AGENTS.md default)', () => {
assert.strictEqual(getProjectInstructionFile('trae'), EXPECTED_PATH);
});
test('capability descriptor declares the same path getProjectInstructionFile returns', () => {
const cap = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'capabilities', 'trae', 'capability.json'), 'utf8'),
);
assert.strictEqual(cap.runtime.hostBehaviors.projectInstructionFile, EXPECTED_PATH);
assert.strictEqual(getProjectInstructionFile('trae'), cap.runtime.hostBehaviors.projectInstructionFile);
});
test('the declared path is a concrete file, not a bare directory (acceptance criterion 2)', () => {
assert.ok(!EXPECTED_PATH.endsWith('/'), 'must not be directory-terminated');
assert.ok(/\.md$/.test(EXPECTED_PATH), 'must name a concrete markdown file');
});
});
describe('#2658: convertClaudeToTraeMarkdown never mutilates the CLAUDE.md path (bin/install.js)', () => {
const cases = [
['bare CLAUDE.md', 'See CLAUDE.md for details.'],
['./CLAUDE.md', 'Read ./CLAUDE.md before starting.'],
['backtick-wrapped `CLAUDE.md`', 'The file `CLAUDE.md` is authoritative.'],
['the exact reported-bug input: .claude/CLAUDE.md', 'Fallback path is .claude/CLAUDE.md by default.'],
['backtick-wrapped .claude/CLAUDE.md', 'Fallback: `.claude/CLAUDE.md`.'],
['./.claude/CLAUDE.md', 'From root: ./.claude/CLAUDE.md'],
];
for (const [label, input] of cases) {
test(`${label} -> ${EXPECTED_PATH}, no malformed output`, () => {
const out = convertClaudeToTraeMarkdown(input);
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
assert.ok(out.includes(EXPECTED_PATH), `output must contain "${EXPECTED_PATH}": ${out}`);
});
}
test('fast-check property: any surrounding text around .claude/CLAUDE.md never yields a malformed path', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 40 }),
fc.string({ maxLength: 40 }),
(prefix, suffix) => {
const content = `${prefix}.claude/CLAUDE.md${suffix}`;
const out = convertClaudeToTraeMarkdown(content);
assert.ok(!out.includes(MALFORMED_SINGLE));
assert.ok(!out.includes(MALFORMED_DOUBLE));
},
),
{ numRuns: 200 },
);
});
});
describe('#2658 defect 5: post-generic-rewrite ".trae/"-prefixed forms preserve their prefix instead of doubling it', () => {
// These simulate the text `copyWithPathReplacement`'s generic `~/.claude/` /
// `$HOME/.claude/` / `./.claude/` -> runtime-dir pass hands to
// convertClaudeToTraeMarkdown — the converter never sees the original
// `.claude/`-prefixed source in this pipeline, only these already-rewritten
// shapes. A fixed-shape patch that only handled the local relative form
// left the local-install-absolute-path and global tilde forms broken.
const cases = [
['local relative (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md', './.trae/rules/rules.md'],
[
'local install absolute path (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/rules/rules.md',
],
['global tilde (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md', '~/.trae/rules/rules.md'],
['backtick-wrapped local relative', '`./.trae/CLAUDE.md`', '`./.trae/rules/rules.md`'],
];
for (const [label, input, expected] of cases) {
test(`${label} -> prefix preserved, no malformed path`, () => {
const out = convertClaudeToTraeMarkdown(input);
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
assert.strictEqual(out, expected);
});
}
test('fast-check property: any arbitrary path ending in .trae/ never yields a doubled prefix', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
(prefix) => {
const content = `${prefix}.trae/CLAUDE.md`;
const out = convertClaudeToTraeMarkdown(content);
assert.ok(!out.includes(MALFORMED_SINGLE));
assert.ok(!out.includes(MALFORMED_DOUBLE));
},
),
{ numRuns: 200 },
);
});
});
describe('#2658 output parity: bin/install.js vs runtime-artifact-conversion.cjs convertClaudeToTraeMarkdown (#2094 mirror)', () => {
// Parity must hold for the pre-existing reported-bug input AND for every
// arbitrary-prefix ".trae/"-tail shape the prefix-preserving regex
// (bin/install.js:2747-2748, mirrored byte-for-byte at
// src/runtime-artifact-conversion.cts:1357-1358) was added to handle. A
// change to only one copy of that regex would otherwise pass every other
// test in this file — none of the defect-5 cases above call the mirror —
// while silently diverging from the other copy.
const parityCases = [
['the reported-bug input (bare .claude/ prefix)', 'Fallback path is .claude/CLAUDE.md by default.'],
['local relative prefix (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md'],
[
'nested project-path absolute prefix (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
],
['global tilde prefix (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md'],
['$HOME-variable prefix (post "$HOME/.claude/" -> "$HOME/.trae/" rewrite)', '$HOME/.trae/CLAUDE.md'],
['backtick-wrapped local relative prefix', '`./.trae/CLAUDE.md`'],
];
for (const [label, input] of parityCases) {
test(`identical output for ${label}`, () => {
assert.strictEqual(
convertClaudeToTraeMarkdown(input),
runtimeArtifactConversion.convertClaudeToTraeMarkdown(input),
);
});
}
test('fast-check property: any arbitrary ".trae/"-tail path produces identical output in both implementations', () => {
fc.assert(
fc.property(
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
(prefix) => {
const content = `${prefix}.trae/CLAUDE.md`;
assert.strictEqual(
convertClaudeToTraeMarkdown(content),
runtimeArtifactConversion.convertClaudeToTraeMarkdown(content),
);
},
),
{ numRuns: 200 },
);
});
});
describe('#2658: end-to-end --trae install never emits the malformed path (acceptance criterion 1)', () => {
test('local install: no emitted .md/.js/.cjs file contains the malformed strings; the rules file is concrete', () => {
const { configDir, root } = runMinimalInstall({ runtime: 'trae', scope: 'local' });
try {
const files = walk(configDir)
.filter((f) => /\.(md|js|cjs)$/.test(f))
// gsd-core/CHANGELOG.md is excluded by exact relative path (not a blanket
// .md skip — the emitted agent/command/workflow markdown this gate exists
// to guard stays fully scanned). CHANGELOG.md legitimately QUOTES the
// malformed `.claude/.trae/rules` / `.trae/.trae/rules` strings while
// documenting the #2658 fix itself (#3006) — that historical-value
// citation is not a regression of the installer's actual output. Verified
// empirically: excluding only this one file drops the hit count to zero
// across all 620 other emitted files.
.filter((f) => f.split(path.sep).join('/').indexOf('gsd-core/CHANGELOG.md') === -1);
assert.ok(files.length > 0, 'expected at least one emitted .md/.js/.cjs file');
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
assert.ok(!content.includes(MALFORMED_SINGLE), `${file} must not contain "${MALFORMED_SINGLE}"`);
assert.ok(!content.includes(MALFORMED_DOUBLE), `${file} must not contain "${MALFORMED_DOUBLE}"`);
}
} finally {
cleanup(root);
}
});
});
describe('#2658 acceptance criterion 3: new-project.md / ingest-docs.md detect trae before falling back to claude', () => {
const workflowsDir = path.join(REPO_ROOT, 'gsd-core', 'workflows');
test('new-project.md recognizes /.trae/ path and TRAE_CONFIG_DIR before the claude fallback', () => {
const content = fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8');
const pathBlock = content.match(/Derive `RUNTIME`[\s\S]*?Otherwise → `RUNTIME=claude`/);
assert.ok(pathBlock, 'runtime-detection path block must exist');
assert.ok(
/Path contains `\/\.trae\/` → `RUNTIME=trae`/.test(pathBlock[0]),
'path-based detection must recognize /.trae/ before the claude fallback',
);
const envBlock = content.match(/if \[ -n "\$CODEX_HOME" \][\s\S]*?else RUNTIME="claude"; fi/);
assert.ok(envBlock, 'env-var fallback block must exist');
assert.ok(
/TRAE_CONFIG_DIR/.test(envBlock[0]),
'env-var fallback must recognize TRAE_CONFIG_DIR before the claude fallback',
);
});
test('ingest-docs.md carries the same trae detection (found during this remediation, not just new-project.md)', () => {
const content = fs.readFileSync(path.join(workflowsDir, 'ingest-docs.md'), 'utf8');
const block = content.match(/\*\*Detect runtime\*\*[\s\S]*?else → `RUNTIME=claude`/);
assert.ok(block, 'runtime-detection block must exist');
assert.ok(
/`\/\.trae\/` → `RUNTIME=trae`/.test(block[0]),
'ingest-docs.md must also recognize /.trae/ before the claude fallback',
);
assert.ok(/TRAE_CONFIG_DIR/.test(content), 'env-var fallback mention must include TRAE_CONFIG_DIR');
});
});
});