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.
237 lines
10 KiB
JavaScript
237 lines
10 KiB
JavaScript
process.env.MSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* Per-plan executor routing via `agent_hint:` frontmatter (#1689, Option A).
|
|
*
|
|
* Coverage:
|
|
* - resolveAgentHint() unit: resolves specialists present in the active
|
|
* runtime's agent dir(s); falls back to null for absent names. Filename
|
|
* variants (.md, .agent.md, .toml).
|
|
* - `msd-tools resolve-agent` route: --raw / --json output, fail-closed to
|
|
* msd-executor.
|
|
* - phase-plan-index data path: `agent_hint` is parsed into plan JSON (null
|
|
* when unset) so the orchestrator reads it from plan_json.
|
|
* - execute-phase.md host wiring: a lean per-plan reference + the
|
|
* `{EXECUTOR_TYPE}` placeholder, with all detail in the step fragment
|
|
* (ADR-857 Phase 6 byte-budget conformance).
|
|
* - workflow.agent_hint_routing config key: default-on (SCHEMA_DEFAULTS),
|
|
* boolean-validated, settable.
|
|
*/
|
|
|
|
const { test, describe, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { runMsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const WORKFLOW_PATH = path.join(ROOT, 'msd-core', 'workflows', 'execute-phase.md');
|
|
const FRAGMENT_PATH = path.join(ROOT, 'msd-core', 'workflows', 'execute-phase', 'steps', 'per-plan-executor-routing.md');
|
|
const { resolveAgentHint } = require('../msd-core/bin/lib/agent-install-check.cjs');
|
|
|
|
// A name unlikely to collide with a real shipped agent, so the global agent dir
|
|
// (~/.claude/agents) never produces a false positive during resolution tests.
|
|
const SPECIALIST = 'zzz-test-specialist-1689';
|
|
const SPECIALIST_TOML = 'zzz-test-specialist-toml-1689';
|
|
|
|
let tmpAgentsRoot;
|
|
let savedAgentsDir;
|
|
|
|
before(() => {
|
|
tmpAgentsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-1689-'));
|
|
savedAgentsDir = process.env.MSD_AGENTS_DIR;
|
|
});
|
|
|
|
after(() => {
|
|
if (savedAgentsDir === undefined) delete process.env.MSD_AGENTS_DIR;
|
|
else process.env.MSD_AGENTS_DIR = savedAgentsDir;
|
|
cleanup(tmpAgentsRoot);
|
|
});
|
|
|
|
describe('#1689 resolveAgentHint() — runtime agent-dir resolution', () => {
|
|
test('empty / whitespace name never resolves (returns null)', () => {
|
|
process.env.MSD_AGENTS_DIR = tmpAgentsRoot;
|
|
assert.equal(resolveAgentHint('', 'claude'), null);
|
|
assert.equal(resolveAgentHint(' ', 'claude'), null);
|
|
});
|
|
|
|
test('resolves a specialist present as <name>.md in the agent dir', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-md-'));
|
|
process.env.MSD_AGENTS_DIR = dir;
|
|
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\nbody\n');
|
|
try {
|
|
assert.equal(resolveAgentHint(SPECIALIST, 'claude'), SPECIALIST);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('resolves a specialist present as <name>.toml (codex variant)', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-toml-'));
|
|
process.env.MSD_AGENTS_DIR = dir;
|
|
fs.writeFileSync(path.join(dir, `${SPECIALIST_TOML}.toml`), 'name = "' + SPECIALIST_TOML + '"\n');
|
|
try {
|
|
assert.equal(resolveAgentHint(SPECIALIST_TOML, 'codex'), SPECIALIST_TOML);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('returns null when the named agent does not resolve (fallback signal)', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-empty-'));
|
|
process.env.MSD_AGENTS_DIR = dir;
|
|
try {
|
|
assert.equal(resolveAgentHint('definitely-not-installed-1689', 'claude'), null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('rejects path-traversing names so they cannot escape the agents dir', () => {
|
|
process.env.MSD_AGENTS_DIR = tmpAgentsRoot;
|
|
assert.equal(resolveAgentHint('../../README', 'claude'), null);
|
|
assert.equal(resolveAgentHint('a/b', 'claude'), null);
|
|
assert.equal(resolveAgentHint('..', 'claude'), null);
|
|
});
|
|
});
|
|
|
|
describe('#1689 msd-tools resolve-agent route', () => {
|
|
test('--raw: echoes the name when the specialist resolves', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-route-'));
|
|
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\n');
|
|
try {
|
|
const r = runMsdTools(['resolve-agent', '--name', SPECIALIST, '--raw'], dir, { MSD_AGENTS_DIR: dir });
|
|
assert.equal(r.exitCode, 0);
|
|
assert.equal(r.output.trim(), SPECIALIST);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('--raw: falls back to msd-executor when the name does not resolve', () => {
|
|
const r = runMsdTools(['resolve-agent', '--name', 'no-such-agent-1689', '--raw'], tmpAgentsRoot, { MSD_AGENTS_DIR: tmpAgentsRoot });
|
|
assert.equal(r.exitCode, 0);
|
|
assert.equal(r.output.trim(), 'msd-executor');
|
|
});
|
|
|
|
test('--raw: falls back to msd-executor for a path-traversing name (fail-closed)', () => {
|
|
const r = runMsdTools(['resolve-agent', '--name', '../../README', '--raw'], tmpAgentsRoot, { MSD_AGENTS_DIR: tmpAgentsRoot });
|
|
assert.equal(r.exitCode, 0);
|
|
assert.equal(r.output.trim(), 'msd-executor');
|
|
});
|
|
|
|
test('--raw: falls back to msd-executor when --name is missing', () => {
|
|
const r = runMsdTools(['resolve-agent', '--raw'], tmpAgentsRoot, { MSD_AGENTS_DIR: tmpAgentsRoot });
|
|
assert.equal(r.exitCode, 0);
|
|
assert.equal(r.output.trim(), 'msd-executor');
|
|
});
|
|
|
|
test('--json: resolved (fallback=false) vs fell-back (fallback=true)', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-agents-json-'));
|
|
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\n');
|
|
try {
|
|
const ok = runMsdTools(['resolve-agent', '--name', SPECIALIST, '--json'], dir, { MSD_AGENTS_DIR: dir });
|
|
const okJson = JSON.parse(ok.output);
|
|
assert.equal(okJson.resolved, SPECIALIST);
|
|
assert.equal(okJson.fallback, false);
|
|
|
|
const bad = runMsdTools(['resolve-agent', '--name', 'no-such-agent-1689', '--json'], dir, { MSD_AGENTS_DIR: dir });
|
|
const badJson = JSON.parse(bad.output);
|
|
assert.equal(badJson.resolved, 'msd-executor');
|
|
assert.equal(badJson.fallback, true);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('#1689 phase-plan-index data path — agent_hint flows into plan JSON', () => {
|
|
function writePlan(projectDir, phase, file, fmFields) {
|
|
const phaseDir = path.join(projectDir, '.planning', 'phases', phase);
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
const entries = Object.entries({ phase, plan: '"01"', type: 'execute', wave: 1, depends_on: '[]', files_modified: '[]', autonomous: true, ...fmFields });
|
|
const fm = entries.map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
const body =
|
|
'---\n' +
|
|
fm + '\n' +
|
|
'must_haves:\n' +
|
|
' truths: []\n' +
|
|
' artifacts: []\n' +
|
|
'---\n# plan\n';
|
|
fs.writeFileSync(path.join(phaseDir, file), body);
|
|
}
|
|
|
|
test('a plan with agent_hint surfaces the value; a plan without surfaces null', () => {
|
|
const project = createTempProject('msd-1689-planidx-');
|
|
try {
|
|
writePlan(project, '01-test', '01-01-PLAN.md', { agent_hint: 'well-me-flutter-engineer' });
|
|
writePlan(project, '01-test', '01-02-PLAN.md', {});
|
|
const r = runMsdTools(['phase-plan-index', '01-test', '--json'], project);
|
|
assert.equal(r.exitCode, 0, r.output);
|
|
const idx = JSON.parse(r.output);
|
|
const byId = Object.fromEntries(idx.plans.map((p) => [p.id, p]));
|
|
assert.equal(byId['01-01'].agent_hint, 'well-me-flutter-engineer');
|
|
assert.equal(byId['01-02'].agent_hint, null);
|
|
} finally {
|
|
cleanup(project);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('#1689 execute-phase.md host wiring (byte-budget-lean; detail in fragment)', () => {
|
|
test('host references the per-plan routing fragment and uses the {EXECUTOR_TYPE} placeholder', () => {
|
|
const host = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
|
|
assert.ok(host.includes('per-plan-executor-routing.md'), 'host must reference the routing fragment');
|
|
assert.ok(host.includes('subagent_type="{EXECUTOR_TYPE}"'), 'host dispatch template must use the {EXECUTOR_TYPE} placeholder');
|
|
// The bulky resolution logic lives in the fragment, NOT inline (ADR-857 Phase 6).
|
|
assert.ok(!/\bmsd_run query resolve-agent\b/.test(host), 'resolution detail (resolve-agent call) must live in the fragment, not the host');
|
|
});
|
|
|
|
test('the routing fragment exists and carries the resolution contract', () => {
|
|
assert.ok(fs.existsSync(FRAGMENT_PATH), 'per-plan-executor-routing.md fragment must exist');
|
|
const frag = fs.readFileSync(FRAGMENT_PATH, 'utf-8');
|
|
assert.ok(frag.includes('EXECUTOR_TYPE'), 'fragment must set EXECUTOR_TYPE');
|
|
assert.ok(frag.includes('msd_run query resolve-agent'), 'fragment must call the resolve-agent query');
|
|
assert.ok(frag.includes('agent_hint'), 'fragment must read plan_json.agent_hint');
|
|
assert.ok(frag.includes('workflow.agent_hint_routing'), 'fragment must honor the config gate');
|
|
// Fallback is the byte-identical default.
|
|
assert.ok(frag.includes('msd-executor'));
|
|
});
|
|
});
|
|
|
|
describe('#1689 workflow.agent_hint_routing config key', () => {
|
|
test('default-on: config-get resolves true in a project that does not set it', () => {
|
|
const project = createTempProject('msd-1689-cfg-');
|
|
try {
|
|
const r = runMsdTools(['config-get', 'workflow.agent_hint_routing', '--raw'], project);
|
|
assert.equal(r.exitCode, 0, r.output);
|
|
assert.equal(r.output.trim(), 'true');
|
|
} finally {
|
|
cleanup(project);
|
|
}
|
|
});
|
|
|
|
test('settable + opt-out round-trip (config-set false then config-get false)', () => {
|
|
const project = createTempProject('msd-1689-cfgset-');
|
|
try {
|
|
const set = runMsdTools(['config-set', 'workflow.agent_hint_routing', 'false'], project);
|
|
assert.equal(set.exitCode, 0, set.output);
|
|
const get = runMsdTools(['config-get', 'workflow.agent_hint_routing', '--raw'], project);
|
|
assert.equal(get.output.trim(), 'false');
|
|
} finally {
|
|
cleanup(project);
|
|
}
|
|
});
|
|
|
|
test('boolean-validated: a non-boolean value is rejected', () => {
|
|
const project = createTempProject('msd-1689-cfgvalid-');
|
|
try {
|
|
const set = runMsdTools(['config-set', 'workflow.agent_hint_routing', 'maybe'], project);
|
|
assert.notEqual(set.exitCode, 0, 'non-boolean must be rejected');
|
|
} finally {
|
|
cleanup(project);
|
|
}
|
|
});
|
|
});
|