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

@@ -52,8 +52,7 @@
* map exists specifically to keep decoupled from the descriptor.
*
* 3. Both scopes are exercised for every runtime that declares a
* per-scope `agents` kind (claude, cline, codex, hermes, kilo, opencode,
* kimi-code × global+local). The prior revision was global-only, which
* per-scope `agents` kind (claude, codex, opencode × global+local). The prior revision was global-only, which
* is exactly the class of gap that let two separate agents-drop
* regressions reach `next` undetected: cline-local (fixed alongside
* this rewrite — capabilities/cline/capability.json's `local`
@@ -170,32 +169,24 @@ function buildTargetDir(commandsMsd) {
/** Hand-verified, independent of any capability.json — this is the thing
* every H row's ACTUAL side (a real capability.json) is checked against.
* `null` means converter:null (identity — claude, kimi-code). */
* `null` means converter:null (identity — claude). */
const EXPECTED_CONVERTER_NAME_BY_RUNTIME = {
claude: null,
'kimi-code': null,
cline: 'convertClaudeAgentToClineAgent',
codex: 'convertClaudeAgentToCodexAgent',
hermes: 'convertClaudeAgentToHermesAgent',
kilo: 'convertClaudeToKiloFrontmatter',
opencode: 'convertClaudeToOpencodeFrontmatter',
};
/** Converter functions callable by name — bin/install.js still owns
* cline/codex/kilo/opencode's (never migrated to runtime-artifact-conversion.cjs);
* hermes's is descriptor-native (#2875 Part 2 / J9-J10). */
* codex/opencode's (never migrated to runtime-artifact-conversion.cjs). */
const NAMED_CONVERTERS = {
convertClaudeAgentToClineAgent: installBin.convertClaudeAgentToClineAgent,
convertClaudeAgentToCodexAgent: installBin.convertClaudeAgentToCodexAgent,
convertClaudeAgentToHermesAgent: runtimeArtifactConversion.convertClaudeAgentToHermesAgent,
convertClaudeToKiloFrontmatter: installBin.convertClaudeToKiloFrontmatter,
convertClaudeToOpencodeFrontmatter: installBin.convertClaudeToOpencodeFrontmatter,
};
/** kilo/opencode take an options bag (`{isAgent, modelOverride}`), resolved
/** opencode takes an options bag (`{isAgent, modelOverride}`), resolved
* ONCE per call via the single shared precedence resolver (J5-J8) — every
* other converter here takes only `content`. */
const MODEL_OVERRIDE_CONVERTER_NAMES = new Set(['convertClaudeToKiloFrontmatter', 'convertClaudeToOpencodeFrontmatter']);
const MODEL_OVERRIDE_CONVERTER_NAMES = new Set(['convertClaudeToOpencodeFrontmatter']);
/**
* Assemble the EXPECTED per-file output for `runtime` from `agentsDir`,
@@ -237,7 +228,7 @@ function computeExpectedOutput(runtime, agentsDir, ctx, converterNameOverride) {
content = fn(content);
}
}
// converter:null (claude, kimi-code) — content unchanged by this step.
// converter:null (claude) — content unchanged by this step.
// Step 4: frontmatter extensions (effort/disallowedTools)
content = runtimeArtifactConversion.applyAgentFrontmatterExtensions(content, { runtime, agentName, targetDir });
// Step 5: normalize colon->hyphen refs
@@ -292,12 +283,8 @@ function runRealDescriptorPath(runtime, scope, targetDir, ctx) {
* sweeps the real registry and fails if a declarant is missing here. */
const RUNTIME_SCOPE_PAIRS = [
['claude', 'global'], ['claude', 'local'],
['cline', 'global'], ['cline', 'local'],
['codex', 'global'], ['codex', 'local'],
['hermes', 'global'], ['hermes', 'local'],
['kilo', 'global'], ['kilo', 'local'],
['opencode', 'global'], ['opencode', 'local'],
['kimi-code', 'global'], ['kimi-code', 'local'],
];
/** Model override literal shared by J8's two `_stageWithModelOverride` calls. */
@@ -400,14 +387,14 @@ test('agent-descriptor-parity: H7 — a filename-only divergence fails the harne
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: H8 — a deliberately-wrong (but real, allowlisted) expected converter turns the harness RED', (t) => {
// Hermes's REAL capability.json declares convertClaudeAgentToHermesAgent.
// OpenCode's REAL capability.json declares convertClaudeToOpencodeFrontmatter.
// Feed the ORACLE a different, real, allowlisted converter name
// (convertClaudeAgentToCodexAgent) instead — simulating exactly the failure
// mode row H exists to catch: capability.json's declared converter silently
// diverging from the correct one. The REAL descriptor path is untouched and
// still uses hermes's real (correct) converter, so this proves: IF
// still uses opencode's real (correct) converter, so this proves: IF
// capability.json ever regressed to the wrong name, THIS harness's H row
// for hermes would go red exactly like this.
// for opencode would go red exactly like this.
const { commandsMsd, agentsDir, root } = buildSourceTree(SAMPLE_AGENTS);
const targetDir = buildTargetDir(commandsMsd);
t.after(() => {
@@ -415,8 +402,8 @@ test('agent-descriptor-parity: H8 — a deliberately-wrong (but real, allowliste
cleanup(targetDir);
});
const ctx = { pathPrefix: `${targetDir}/`, attribution: undefined, targetDir };
const wrongExpected = computeExpectedOutput('hermes', agentsDir, ctx, 'convertClaudeAgentToCodexAgent');
const realActual = runRealDescriptorPath('hermes', 'global', targetDir, ctx);
const wrongExpected = computeExpectedOutput('opencode', agentsDir, ctx, 'convertClaudeAgentToCodexAgent');
const realActual = runRealDescriptorPath('opencode', 'global', targetDir, ctx);
let threw = false;
let observedDiff = null;
try {
@@ -514,7 +501,7 @@ test('agent-descriptor-parity: J4 — disallowedTools injected only on a READONL
});
// ---------------------------------------------------------------------------
// J5-J8 — model-override resolution (kilo/opencode), single-sourced
// J5-J8 — model-override resolution (opencode), single-sourced
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: J5 — explicit model_overrides[agent] wins (highest precedence)', () => {
@@ -541,61 +528,22 @@ test('agent-descriptor-parity: J7 — neither configured resolves to null (omit)
assert.doesNotMatch(contentWithoutModelOverride, /^model:/m, 'an omitted override must not appear as a model: key at all');
});
test('agent-descriptor-parity: J8 — kilo and opencode both resolve model overrides through the REAL descriptor path (installAgentsKindStandalone), proving ONE shared resolution path, not a tautology', () => {
// #2875 Part 2 defect fix: a prior revision of this row called
// resolveAgentModelOverride TWICE with identical arguments and asserted
// equality — true of ANY pure function regardless of whether kilo/opencode
// are actually wired to it. This revision drives BOTH runtimes through the
// REAL production entry point with a REAL .planning/config.json
// model_overrides block and asserts BOTH staged outputs carry the SAME
// resolved model — which only happens if both are genuinely wired to the
// one shared installModelOverrideResolver.resolveAgentModelOverride.
const kiloOut = _stageWithModelOverride('kilo', J8_OVERRIDE_MODEL);
test('agent-descriptor-parity: J8 — opencode resolves model overrides through the REAL descriptor path (installAgentsKindStandalone)', () => {
// Drives the REAL production entry point with a REAL .planning/config.json
// model_overrides block and asserts the staged output carries the resolved
// model — which only happens if the runtime is genuinely wired to the
// shared installModelOverrideResolver.resolveAgentModelOverride.
const opencodeOut = _stageWithModelOverride('opencode', J8_OVERRIDE_MODEL);
// J8_OVERRIDE_MODEL is a fixed constant: assert the exact expected
// frontmatter line, matched line-wise, rather than building a RegExp from
// an interpolated value (CodeQL js/incomplete-sanitization — a
// metachar-bearing value would silently widen the match).
// an interpolated value (CodeQL js/incomplete-sanitization).
const expectedModelLine = `model: ${J8_OVERRIDE_MODEL}`;
assert.ok(
kiloOut.get('msd-planner.md').split('\n').some((line) => line.trim() === expectedModelLine),
'kilo must apply the shared model override via the real descriptor path',
);
assert.ok(
opencodeOut.get('msd-planner.md').split('\n').some((line) => line.trim() === expectedModelLine),
'opencode must apply the SAME shared model override via the real descriptor path',
'opencode must apply the shared model override via the real descriptor path',
);
});
// ---------------------------------------------------------------------------
// J9-J10 — hermes branding converter
// ---------------------------------------------------------------------------
test('agent-descriptor-parity: J9 — hermes branding converter is byte-identical to the shared generic branding-rewrite function, including \\bClaude Code\\b word-boundary semantics', () => {
const content = 'Claude Code and ClaudeCodeExtra and CLAUDE.md and .claude/foo and reClaude Code.\n';
const viaSharedFn = runtimeArtifactConversion.applyAgentBrandingRewrites(content, 'hermes');
const viaNamedConverter = runtimeArtifactConversion.convertClaudeAgentToHermesAgent(content);
assert.equal(viaSharedFn, viaNamedConverter, 'the named converter must be a pure delegate to the generic branding-rewrite function');
// \bClaude Code\b: neither "ClaudeCodeExtra" (no space/boundary between
// "Claude" and "Code") nor "reClaude Code" (no boundary between the 'e' of
// "re" and the 'C' of "Claude" — both word chars) satisfy the word-boundary
// requirement, so BOTH are left untouched; only the standalone occurrence is
// rewritten.
assert.match(viaSharedFn, /Hermes Agent and ClaudeCodeExtra and HERMES\.md and \.hermes\/foo and reClaude Code\./);
});
test('agent-descriptor-parity: J10 — the branding converter is descriptor-data-driven, not hardcoded to hermes strings', () => {
const content = 'Claude Code uses CLAUDE.md under .claude/.\n';
// A runtime with NO brandingRewrites declared gets nothing rewritten.
assert.equal(runtimeArtifactConversion.applyAgentBrandingRewrites(content, 'claude'), content);
// hermes (the only runtime with brandingRewrites AND no dedicated converter
// pre-#2875) gets ITS OWN declared rewrite table applied — proving the
// function reads the runtime's descriptor rather than a hermes-hardcoded literal.
const hermesOut = runtimeArtifactConversion.applyAgentBrandingRewrites(content, 'hermes');
assert.notEqual(hermesOut, content);
assert.match(hermesOut, /Hermes Agent uses HERMES\.md under \.hermes\/\./);
});
// ---------------------------------------------------------------------------
// K1 — migration completeness: every registry runtime with an `agents` kind
// is reachable from the REAL production entry point, not the (now-deleted)
@@ -629,9 +577,8 @@ test('agent-descriptor-parity: K1 — every registry runtime declaring an agents
})
.map(([id]) => id);
assert.ok(runtimesWithAgentsKind.length >= 7, 'sanity: expected at least the seven #2875 Part 2 runtimes to declare an agents kind');
assert.ok(runtimesWithAgentsKind.includes('kimi-code'), 'kimi-code (found via golden fixture, not analysis) must be covered here');
assert.ok(runtimesWithAgentsKind.includes('cline'), 'cline must be covered here');
assert.ok(runtimesWithAgentsKind.length >= 3, 'sanity: expected at least the kept runtimes to declare an agents kind');
assert.ok(runtimesWithAgentsKind.includes('opencode'), 'opencode must be covered here');
// #3712: this loop reaches `codex`, whose global skills kind declares a `home`
// override (`.agents`) resolved from os.homedir() rather than from targetDir.