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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user