refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity

Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
This commit is contained in:
Jakub Zych
2026-10-06 20:02:40 +02:00
parent 12ee75a509
commit 6cfa0c55d2
509 changed files with 1869 additions and 47737 deletions

View File

@@ -1,12 +1,12 @@
/**
* Runtime Converter Tests — OpenCode + Kilo
* Runtime Converter Tests — OpenCode
*
* Tests for small runtime-specific conversion functions from install.js.
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
* Larger runtime test suites (Codex, Antigravity) have their own files.
*
* OpenCode/Kilo: flat-runtime frontmatter converters (agent + command modes)
* OpenCode: flat-runtime frontmatter converter (agent + command modes)
* model: inherit is NOT added (runtime uses its configured default model)
* but mode: subagent IS added (required by both runtimes' agents).
* but mode: subagent IS added (required by the runtime's agents).
*/
const { test, describe } = require('node:test');
@@ -17,17 +17,12 @@ const path = require('node:path');
process.env.MSD_TEST_MODE = '1';
const {
convertClaudeToOpencodeFrontmatter,
convertClaudeToKiloFrontmatter,
convertClaudeAgentToAntigravityAgent,
convertClaudeCommandToTraeSkill,
convertClaudeCommandToKimiSkill,
buildKimiAgentArtifacts,
neutralizeAgentReferences,
} = require('../bin/install.js');
const {
convertClaudeCommandToOpencodeSkill,
convertClaudeCommandToKiloSkill,
} = require('../msd-core/bin/lib/install-engine.cjs');
// Sample Claude agent frontmatter (matches actual MSD agent format)
@@ -68,63 +63,40 @@ const flatRuntimeSuites = [
convert: convertClaudeToOpencodeFrontmatter,
configDir: '.config/opencode',
},
{
label: 'Kilo',
convert: convertClaudeToKiloFrontmatter,
configDir: '.config/kilo',
},
];
describe('#4482: OpenCode conversion strips Copilot-only runtime notes', () => {
describe('#4482: OpenCode conversion strips runtime notes declared for another runtime', () => {
const convert = (body) => liveConversion.convertClaudeToOpencodeFrontmatter(
['---', 'name: msd-test', 'description: test', '---', body].join('\n'),
);
test('removes a Copilot-only note without leaving an empty wrapper', () => {
test('removes a note declared for another runtime without leaving an empty wrapper', () => {
const out = convert([
'<runtime_note>',
'**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`.',
'<runtime_note runtime="claude">',
'**Claude Code:** Use `AskUserQuestion` for interactive prompts.',
'</runtime_note>',
'',
'<objective>Keep me.</objective>',
].join('\n'));
assert.ok(!out.includes('vscode_askquestions'));
assert.ok(!out.includes('<runtime_note>'));
assert.ok(!out.includes('interactive prompts'));
assert.ok(!out.includes('<runtime_note'));
assert.ok(out.includes('<objective>Keep me.</objective>'));
});
test('preserves runtime-neutral fallback text in a mixed note', () => {
const out = convert([
'<runtime_note>',
'**Copilot (VS Code):** Use `vscode_askquestions` instead of `AskUserQuestion`.',
'',
'**TEXT_MODE fallback:** Present a numbered list when interactive tools are unavailable.',
'</runtime_note>',
].join('\n'));
assert.ok(!out.includes('vscode_askquestions'));
assert.match(out, /<runtime_note>\n\*\*TEXT_MODE fallback:\*\*/);
assert.ok(out.includes('</runtime_note>'));
});
test('leaves unrelated runtime notes intact', () => {
const note = '<runtime_note>\n**OpenCode:** Keep this runtime-specific guidance.\n</runtime_note>';
assert.ok(convert(note).includes(note));
});
test('the shared audience filter covers every non-Copilot runtime and preserves Copilot', () => {
const note = '<runtime_note>\n**Copilot (VS Code):** Use `vscode_askquestions`.\n\n**TEXT_MODE fallback:** Keep me.\n</runtime_note>';
const runtimes = [
'antigravity', 'augment', 'claude', 'cline', 'codebuddy', 'codex', 'cursor', 'hermes',
'kilo', 'kimi', 'kimi-code', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode',
];
test('the shared audience filter drops a declared-audience note for every other runtime and preserves it for its audience', () => {
const note = '<runtime_note runtime="claude">\n**Claude Code:** Only Claude sees this.\n</runtime_note>';
const runtimes = ['antigravity', 'codex', 'cursor', 'opencode', 'zcode'];
for (const runtime of runtimes) {
const out = liveConversion.filterRuntimeNotesForTarget(note, runtime);
assert.ok(!out.includes('vscode_askquestions'), `${runtime} must not receive the Copilot note`);
assert.ok(out.includes('TEXT_MODE fallback'), `${runtime} must retain neutral fallback guidance`);
assert.ok(!out.includes('Only Claude sees this'), `${runtime} must not receive the Claude-declared note`);
}
assert.strictEqual(liveConversion.filterRuntimeNotesForTarget(note, 'copilot'), note);
assert.ok(liveConversion.filterRuntimeNotesForTarget(note, 'claude').includes('Only Claude sees this'));
});
test('a real OpenCode install filters mvp-phase workflow assets too', (t) => {
@@ -164,18 +136,7 @@ for (const { label, convert, configDir } of flatRuntimeSuites) {
const frontmatter = result.split('---')[1];
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
if (label === 'Kilo') {
assert.ok(frontmatter.includes('permission:'), 'Kilo agents should emit permission block');
assert.ok(frontmatter.includes('read: allow'), 'Read should map to read: allow');
assert.ok(frontmatter.includes('edit: allow'), 'Write/Edit should map to edit: allow');
assert.ok(frontmatter.includes('bash: allow'), 'Bash should map to bash: allow');
assert.ok(frontmatter.includes('grep: allow'), 'Grep should map to grep: allow');
assert.ok(frontmatter.includes('glob: allow'), 'Glob should map to glob: allow');
assert.ok(frontmatter.includes('task: deny'), 'unspecified permissions should be denied');
} else {
assert.ok(!frontmatter.includes('permission:'), 'OpenCode agents should not emit permission block');
}
assert.ok(!frontmatter.includes('permission:'), 'OpenCode agents should not emit permission block');
});
test('strips skills: array', () => {
@@ -226,14 +187,6 @@ Fallback skills live in .agents/skills/.`;
const result = convert(agentWithClaudePaths, { isAgent: true });
assert.ok(result.includes(`~/${configDir}/agent-memory/`), '~/.claude should be replaced');
assert.ok(result.includes(`$HOME/${configDir}/skills/`), '$HOME/.claude should be replaced');
if (label === 'Kilo') {
assert.ok(result.includes('.kilo/skills/'), '.claude/skills should be replaced for Kilo');
assert.ok(result.includes('.kilo/agents/'), '.claude/agents should be replaced for Kilo');
assert.ok(result.includes('./.kilo/hooks/'), './.claude should be replaced for Kilo');
assert.ok(result.includes('Fallback skills live in .kilo/skills/.'), '.agents/skills should be rewritten to Kilo skills dir');
assert.ok(!result.includes('.kilo/skill/'), 'singular Kilo skill dir should not be emitted');
}
});
});
@@ -258,9 +211,7 @@ Fallback skills live in .agents/skills/.`;
});
});
// ─── #2256: model_overrides support for OpenCode/Kilo agents ────────────────
// Only test OpenCode — Kilo uses the same converter but model override injection
// is wired only for OpenCode at the call site in install().
// ─── #2256: model_overrides support for OpenCode agents ────────────────────
if (label === 'OpenCode') {
describe('OpenCode agent model override (modelOverride option) (#2256)', () => {
test('adds model: field when modelOverride is provided', () => {
@@ -301,48 +252,10 @@ Fallback skills live in .agents/skills/.`;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeToKiloFrontmatter is
// defined TWICE — once in bin/install.js (the function bound at the top of this
// file, used by bin/install.js's own legacy install path) and once in
// src/runtime-artifact-conversion.cts, compiled to
// msd-core/bin/lib/runtime-artifact-conversion.cjs (used by
// src/install-engine.cts's newer TS install path, see install-engine.cts ~L871).
// Both copies are LIVE — neither re-exports the other — so #2093's modelOverride
// edit had to be applied twice by hand. Source-text identity can't be asserted
// (they live in different module systems: plain CJS vs a tsc-compiled .cts
// output with different surrounding comments), so this instead proves the two
// implementations still produce IDENTICAL output for representative agent and
// command input. If a future edit changes one copy's behavior without mirroring
// it into the other, this test is the guard that catches the divergence.
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeToKiloFrontmatter output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2093)', () => {
const { convertClaudeToKiloFrontmatter: convertViaConversionModule } =
require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
test('identical output for an agent with a model override', () => {
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
const viaModule = convertViaConversionModule(SAMPLE_AGENT, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical agent output');
});
test('identical output for an agent with no model override', () => {
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
const viaModule = convertViaConversionModule(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical agent output with no override');
});
test('identical output for a command (model override never applies)', () => {
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_COMMAND, { isAgent: false, modelOverride: 'x' });
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, { isAgent: false, modelOverride: 'x' });
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical command output');
});
});
describe('convertClaudeToOpencodeFrontmatter output parity: bin/install.js vs runtime-artifact-conversion.cjs (#4482)', () => {
const { convertClaudeToOpencodeFrontmatter: convertViaConversionModule } =
require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
const input = `${SAMPLE_COMMAND}\n\n<runtime_note>\n**Copilot (VS Code):** Use vscode_askquestions.\n</runtime_note>`;
const input = `${SAMPLE_COMMAND}\n\n<runtime_note runtime="claude">\n**Claude Code:** Claude-only guidance.\n</runtime_note>`;
test('published and module converters filter runtime notes identically', () => {
assert.equal(
@@ -352,100 +265,6 @@ describe('convertClaudeToOpencodeFrontmatter output parity: bin/install.js vs ru
});
});
// ─────────────────────────────────────────────────────────────────────────────
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeCommandToTraeSkill is
// defined TWICE — once in bin/install.js (dead for the live skills-install
// path; kept for this file's own module-level export/test surface) and once in
// src/runtime-artifact-conversion.cts, compiled to
// msd-core/bin/lib/runtime-artifact-conversion.cjs (used by
// src/install-engine.cts's skills-install path via SKILLS_CONVERTER_REGISTRY,
// see install-engine.cts ~L754). Both copies are LIVE call surfaces — neither
// re-exports the other — so #2094's `stage:` emission had to be applied to
// bin/install.js's copy by hand to keep parity. Source-text identity can't be
// asserted (they live in different module systems: plain CJS vs a
// tsc-compiled .cts output with different surrounding comments), so this
// instead proves the two implementations still produce IDENTICAL output for
// representative command input. If a future edit changes one copy's behavior
// without mirroring it into the other, this test is the guard that catches
// the divergence.
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeCommandToTraeSkill output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2094)', () => {
const { convertClaudeCommandToTraeSkill: convertViaConversionModule } =
require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
test('identical output for a representative command, including the #2094 stage: field', () => {
const viaInstall = convertClaudeCommandToTraeSkill(SAMPLE_COMMAND, 'msd-execute-phase');
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, 'msd-execute-phase');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical command output');
assert.match(viaInstall, /\nstage: workflow\n/, 'both copies must emit the #2094 stage: field');
});
test('identical output when the source has no description (falls back to generic description)', () => {
const noDescriptionCommand = `---
name: msd-noop
allowed-tools:
- Read
---
Do nothing.`;
const viaInstall = convertClaudeCommandToTraeSkill(noDescriptionCommand, 'msd-noop');
const viaModule = convertViaConversionModule(noDescriptionCommand, 'msd-noop');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical output when description is absent');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeCommandToKimiSkill
// and buildKimiAgentArtifacts are each defined TWICE — once in bin/install.js
// (dead for the live install path; kept for this file's own module-level
// export/test surface) and once in src/runtime-artifact-conversion.cts,
// compiled to msd-core/bin/lib/runtime-artifact-conversion.cjs (used by
// src/install-engine.cts's skills-install path via SKILLS_CONVERTER_REGISTRY
// for skills, and by src/runtime-artifact-layout.cts's kimiAgentsKind for
// agent YAML — see runtime-artifact-layout.cts ~L268/L289). Both copies are
// LIVE call surfaces — neither re-exports the other, so any future
// kimi-specific fix (e.g. the #2095 code-review fixes) must be applied to
// both by hand. Source-text identity can't be asserted (different module
// systems: plain CJS vs a tsc-compiled .cts output with different
// surrounding comments), so this instead proves the two implementations
// still produce IDENTICAL output for representative command and agent
// input. If a future edit changes one copy's behavior without mirroring it
// into the other, this test is the guard that catches the divergence.
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeCommandToKimiSkill / buildKimiAgentArtifacts output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2095)', () => {
const {
convertClaudeCommandToKimiSkill: convertViaConversionModule,
buildKimiAgentArtifacts: buildViaConversionModule,
} = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
test('identical skill output for a representative command', () => {
const viaInstall = convertClaudeCommandToKimiSkill(SAMPLE_COMMAND, 'msd-execute-phase');
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, 'msd-execute-phase');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical Kimi skill output');
});
test('identical skill output when the source has no description (falls back to generic description)', () => {
const noDescriptionCommand = `---
name: msd-noop
allowed-tools:
- Read
---
Do nothing.`;
const viaInstall = convertClaudeCommandToKimiSkill(noDescriptionCommand, 'msd-noop');
const viaModule = convertViaConversionModule(noDescriptionCommand, 'msd-noop');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical output when description is absent');
});
test('identical agent-artifact output for a representative agent with a subagent, including the Agent tool grant', () => {
const viaInstall = buildKimiAgentArtifacts({ rootAgent: SAMPLE_AGENT, subagents: [SAMPLE_AGENT] });
const viaModule = buildViaConversionModule({ rootAgent: SAMPLE_AGENT, subagents: [SAMPLE_AGENT] });
assert.deepEqual(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit an identical artifact bundle');
assert.match(viaInstall.root.yaml, /kimi_cli\.tools\.agent:Agent/,
'both copies must grant the Agent tool when a subagent is present (#2095 Upgrade 2)');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Antigravity agent conversion — shared Gemini-backend tool mapping (#1394 / #1928)
// ─────────────────────────────────────────────────────────────────────────────
@@ -540,7 +359,7 @@ describe('neutralizeAgentReferences', () => {
});
});
// ─── OpenCode-family skill converters (SKILL.md) — #784 ──────────────────────
// ─── OpenCode skill converter (SKILL.md) — #784 ──────────────────────────────
const SKILL_SAMPLE_COMMAND = `---
description: Show available MSD commands and usage guide
@@ -560,10 +379,9 @@ description: "[BETA] Offload plan phase to the cloud and import back."
Body for /msd:ultraplan-phase.
`;
describe('convertClaudeCommandToOpencodeSkill / convertClaudeCommandToKiloSkill (#784)', () => {
describe('convertClaudeCommandToOpencodeSkill (#784)', () => {
const cases = [
{ label: 'opencode', convert: convertClaudeCommandToOpencodeSkill },
{ label: 'kilo', convert: convertClaudeCommandToKiloSkill },
];
for (const { label, convert } of cases) {
@@ -655,9 +473,8 @@ const { cleanup } = require('./helpers.cjs');
/**
* Minimal Claude agent source with comma-separated tools (Claude format).
* Copilot conversion turns tools into a JSON array (CONV-04/05).
* Codex conversion adds <codex_agent_role> block.
* Cursor/Windsurf/Augment/Trae/Codebuddy/Cline conversion strips color field.
* Cursor conversion strips color field.
*/
const CLAUDE_AGENT_SOURCE = `---
name: msd-planner
@@ -795,39 +612,6 @@ describe('feat-1173: stageAgentsForRuntimeWithConverter', () => {
// ─── dispatchKindEntry wiring tests ──────────────────────────────────────────
describe('feat-1173: dispatchKindEntry agents converter wiring', () => {
test('agents kind with convertClaudeAgentToCopilotAgent converter applies copilot conversion', (t) => {
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
cleanup(fixtureRoot);
cleanupStagedSkills();
});
const registry = makeSyntheticRegistry('convertClaudeAgentToCopilotAgent');
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry, 'testruntime', fixtureRoot, 'global',
);
assert.strictEqual(layout.kinds.length, 1);
const agentKind = layout.kinds[0];
assert.strictEqual(agentKind.kind, 'agents');
const resolvedProfile = { name: 'full', skills: '*', agents: new Set() };
const stagedDir = agentKind.stage(resolvedProfile);
const stagedFile = path.join(stagedDir, 'msd-planner.md');
assert.ok(fs.existsSync(stagedFile), `staged file must exist: ${stagedFile}`);
const stagedContent = fs.readFileSync(stagedFile, 'utf8');
// Copilot CONV-04/05: tools converted from "Bash, Read, Write" to JSON array "['bash', 'read', 'write']"
// Raw copy would keep the original comma-separated "tools: Bash, Read, Write" line.
assert.notStrictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter must have transformed the content');
assert.ok(
stagedContent.includes("tools: ['") || stagedContent.includes('tools: ['),
`Copilot conversion must produce JSON array tools. Got:\n${stagedContent.slice(0, 300)}`,
);
});
test('agents kind with convertClaudeAgentToCodexAgent converter applies codex conversion', (t) => {
const fixtureRoot = makeFixtureRoot([{ name: 'msd-planner.md', content: CLAUDE_AGENT_SOURCE }]);
t.after(() => {
@@ -905,7 +689,7 @@ describe('feat-1173: dispatchKindEntry agents converter wiring', () => {
test('scope threads isGlobal to a scope-aware converter (global vs local differ)', (t) => {
// The plumbing kept by #1173 (option a): convertedAgentsKind / dispatchKindEntry
// pass the install scope to the converter as isGlobal. A scope-aware converter
// (copilot) must therefore produce different output for global vs local. This
// (antigravity) must therefore produce different output for global vs local. This
// proves the thread is live via a synthetic descriptor — no real runtime
// declares a converted agents kind yet (declarations deferred to the ADR-1235
// §0 parity follow-up).
@@ -921,7 +705,7 @@ describe('feat-1173: dispatchKindEntry agents converter wiring', () => {
prefix: 'msd-',
nesting: 'flat',
recursive: false,
converter: 'convertClaudeAgentToCopilotAgent',
converter: 'convertClaudeAgentToAntigravityAgent',
};
const registry = {
runtimes: { testruntime: { runtime: { artifactLayout: { global: [agentsEntry], local: [agentsEntry] } } } },
@@ -1202,8 +986,8 @@ test('parity: every non-Claude runtime stamps its own runtime default, and use_w
// #2652: the use_worktrees=false stamp is scoped to the runtimes whose
// negotiated dispatch.isolation really is `none`. A host that declares
// harness-worktree (cursor) or orchestrator-worktree (codex, opencode,
// kimi, kimi-code) must keep the unstamped `true` default, or the stamp
// harness-worktree (cursor) or orchestrator-worktree (codex, opencode)
// must keep the unstamped `true` default, or the stamp
// resolves USE_WORKTREES=false before dispatch-isolation is consulted and
// the runtime is judged by its name after all.
if (isolation === 'none') {
@@ -1677,9 +1461,9 @@ test('manager.md and autonomous.md no longer contain old "not claude" background
describe('#2486 regression: settings/health worktrees isolation branch', () => {
// Review round 2 (#2584 Phase 3): isolation is a DECLARED CAPABILITY, not a
// runtime name. cursor declares harness-worktree and codex/opencode/kimi/
// kimi-code declare orchestrator-worktree, so a `RUNTIME != claude` gate
// would block a supported configuration on five runtimes and false-warn in
// runtime name. cursor declares harness-worktree and codex/opencode
// declare orchestrator-worktree, so a `RUNTIME != claude` gate
// would block a supported configuration on several runtimes and false-warn in
// health. Both surfaces branch on `dispatch-isolation` instead.
test('settings.md and health.md contain NO runtime-name gate for the worktrees branch', () => {
// allow-test-rule: source-text-is-the-product (#2486)
@@ -1923,7 +1707,7 @@ test('manager.md and autonomous.md no longer contain old "not claude" background
return res.stdout.includes('W025:');
};
// The reviewer's exact repro: rt=qwen, source (un-stamped) emit, cfg={}.
// The reviewer's exact repro: an isolation=none runtime, source (un-stamped) emit, cfg={}.
// The key is absent, so `config-get` prints nothing. This FIRED before.
assert.equal(
fire('none', ''),
@@ -2111,7 +1895,7 @@ test('manager.md and autonomous.md no longer contain old "not claude" background
*
* YAML 1.2 treats a leading `[` as the start of a flow sequence, so any
* downstream parser (gh-copilot, JetBrains' kit, etc.) fails with
* "Unexpected scalar at node end". The Copilot/Antigravity/Trae/Codebuddy
* "Unexpected scalar at node end". The Antigravity
* skill+agent converters in `bin/install.js` re-emit the description
* unquoted; the Claude variant `yamlQuote(...)`s it. Bring the others
* in line so any value is round-trip-safe regardless of leading char.
@@ -2237,14 +2021,10 @@ function assertDescriptionRoundTrips(emitted, expected, label) {
}
const COMMAND_CONVERTERS = [
{ label: 'convertClaudeCommandToCopilotSkill', fn: (src) => install.convertClaudeCommandToCopilotSkill(src, 'msd-ultraplan-phase') },
{ label: 'convertClaudeCommandToAntigravitySkill', fn: (src) => install.convertClaudeCommandToAntigravitySkill(src, 'msd-ultraplan-phase') },
{ label: 'convertClaudeCommandToTraeSkill', fn: (src) => install.convertClaudeCommandToTraeSkill(src, 'msd-ultraplan-phase') },
{ label: 'convertClaudeCommandToCodebuddySkill', fn: (src) => install.convertClaudeCommandToCodebuddySkill(src, 'msd-ultraplan-phase') },
];
const AGENT_CONVERTERS = [
{ label: 'convertClaudeAgentToCopilotAgent', fn: (src) => install.convertClaudeAgentToCopilotAgent(src) },
{ label: 'convertClaudeAgentToAntigravityAgent', fn: (src) => install.convertClaudeAgentToAntigravityAgent(src) },
];
@@ -2253,11 +2033,8 @@ const AGENT_CONVERTERS = [
// rest defend against neighbouring drift.
const FLOW_HOSTILE_PREFIXES = ['[', '{', '*', '&', '!', '|', '>', '%', '@', '`'];
// Some converters (Trae, CodeBuddy) deliberately rewrite "Claude Code"
// in body content to their target runtime name, and the rewrite cuts
// across the description too. That's correct behavior — out of scope for
// the YAML-quoting fix — so for the reporter case we assert only the
// quoting requirement, not byte-equality of the round-tripped value.
// For the reporter case we assert only the quoting requirement, not
// byte-equality of the round-tripped value.
function assertDescriptionIsQuoted(emitted, label) {
const fmLines = extractFrontmatter(emitted);
const descLine = findDescriptionLine(fmLines);
@@ -2311,7 +2088,6 @@ describe('bug-2876: skill+agent converters emit YAML-quoted description', () =>
const liveConversion = require('../msd-core/bin/lib/runtime-artifact-conversion.cjs');
const OPENCODE_CONVERTERS = [['src (live bake)', liveConversion.convertClaudeToOpencodeFrontmatter]];
const KILO_CONVERTERS = [['src (live bake)', liveConversion.convertClaudeToKiloFrontmatter]];
describe('#3706: OpenCode agent variant (resolved effort)', () => {
const AGENT = ['---', 'name: msd-executor', 'description: x', 'model: sonnet', '---', '', 'Body.'].join('\n');
@@ -2353,17 +2129,6 @@ describe('#3706: OpenCode agent variant (resolved effort)', () => {
);
});
}
for (const [label, convert] of KILO_CONVERTERS) {
test(`[${label}] Kilo does NOT emit variant even when one is passed`, () => {
// The asymmetry, pinned. EFFORT_ARGV declares surfaces for claude/opencode/
// codex and has no kilo entry — unlike the model side, where #2794 J8
// requires the two runtimes to resolve identically.
const out = convert(AGENT, { isAgent: true, modelOverride: 'M', variant: 'high' });
assert.ok(!hasKey(out, 'variant'), `kilo has no declared effort surface; got:\n${out}`);
assert.match(out, /^model: M$/m, 'but the model override still applies');
});
}
});
// ─── #3706: config-supplied values cannot inject frontmatter keys ────────────
@@ -2392,13 +2157,6 @@ describe('#3706: frontmatter values are escaped, not interpolated raw', () => {
assert.match(out, /^variant: "/m);
});
}
for (const [label, convert] of KILO_CONVERTERS) {
test(`[${label}] Kilo model values are escaped too — the same sink exists there`, () => {
const out = convert(AGENT, { isAgent: true, modelOverride: 'sonnet\ntools: ["*"]' });
assert.ok(!/^tools:/m.test(out), `injected a tools key:\n${out}`);
});
}
});
// ─── #3706: values that look plain but do not round-trip ────────────────────