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

@@ -24,7 +24,7 @@ import os from 'node:os';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installFsAdapter = require('./install-fs-adapter.cjs');
import { tryWithinRootLexical } from './security.cjs';
const { installFs, mkInstallTempDir } = installFsAdapter;
const { installFs } = installFsAdapter;
// Reuse the install manifest's existing parser and streamed SHA-256
// classification instead of deriving a second integrity implementation here.
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -43,7 +43,7 @@ const conversionExports = runtimeArtifactConversion as Record<string, unknown> &
readMsdCommandNames?: () => string[];
};
// #2875 Part 2 (J8): shared model-override precedence resolver — see its
// module doc for why kilo/opencode MUST resolve through this ONE function
// module doc for why opencode MUST resolve through this ONE function
// rather than re-deriving the chain per runtime.
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installModelOverrideResolver = require('./install-model-override-resolver.cjs');
@@ -71,7 +71,6 @@ const _require: NodeRequire = require;
// ---------------------------------------------------------------------------
type ArtifactKindName = 'commands' | 'agents' | 'skills';
type KimiArtifactKindName = ArtifactKindName | 'kimi-agents';
// Mirrors the (unexported) ResolvedProfile in install-profiles.cts.
// Must stay in sync if that shape changes.
@@ -105,7 +104,7 @@ interface AgentCtx {
pathPrefix: string;
attribution: string | null | undefined;
/** #2875 Part 2 (row I1-I3): install root, threaded through to the
* frontmatter-extensions step and (for kilo/opencode's converters) the
* frontmatter-extensions step and (for opencode's converter) the
* per-agent model-override resolution below. Mirrors install-profiles.cts's
* identically-named AgentCtx field — see its doc comment. */
targetDir?: string | null;
@@ -114,7 +113,7 @@ interface AgentCtx {
}
interface ArtifactKind {
kind: KimiArtifactKindName;
kind: ArtifactKindName;
destSubpath: string;
prefix: string;
/** For agent kinds, accepts optional pre-converter cross-cutting context
@@ -152,7 +151,7 @@ function requiredRuntimeSurfaceSourceClasses(kinds: Iterable<{ kind?: string }>)
const required = new Set<RuntimeSurfaceSourceClass>();
for (const kind of kinds) {
if (kind.kind === 'commands' || kind.kind === 'skills') required.add('commands');
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') required.add('agents');
if (kind.kind === 'agents') required.add('agents');
}
return required;
}
@@ -524,7 +523,7 @@ function _resolveNamedConverter(converterName: string, kindLabel: string): (...a
/**
* Build a converted-agents kind descriptor for runtimes whose agent `.md` files
* need runtime-specific frontmatter/body conversion (e.g. Copilot, Cursor, Codex).
* need runtime-specific frontmatter/body conversion (e.g. Cursor, Codex).
*
* Unlike `agentsKind` (which raw-copies source files), this kind applies
* `converterName` from Runtime Artifact Conversion exports to each agent file
@@ -538,8 +537,8 @@ function _resolveNamedConverter(converterName: string, kindLabel: string): (...a
*
* Of the four blockers this comment used to name for wiring `bin/install.js`'s
* inline agent loop against this resolver, THREE were already stale by the
* time #2875 measured them and are not re-litigated here: Copilot's
* `.agent.md` rename (the loop's own `destName = entry.name` comment records
* time #2875 measured them and are not re-litigated here: the agent-file
* extension rename (the loop's own `destName = entry.name` comment records
* the ternary dropped in #2099; the descriptor fold applies it via
* `hostBehaviors.agentFileExtension`), the cross-cutting path-prefix rewrite +
* attribution (`stageAgentsForRuntimeWithConverter` already applies
@@ -557,10 +556,8 @@ function _resolveNamedConverter(converterName: string, kindLabel: string): (...a
* `disallowedTools` injection) and (b) let THIS function resolve a per-agent
* model override (`installModelOverrideResolver.resolveAgentModelOverride`,
* `model_overrides[agent]` > `model_profile_overrides.<rt>.<tier>` > omit)
* before invoking a converter that needs it (kilo/opencode). Both pieces —
* plus a data-driven Hermes branding converter
* (`convertClaudeAgentToHermesAgent`, reading `hostBehaviors.brandingRewrites`
* rather than a hardcoded string table) — are single-sourced: `bin/install.js`
* before invoking a converter that needs it (opencode). Both pieces are
* single-sourced: `bin/install.js`
* requires the SAME functions this module does, so its inline loop and the
* descriptor path can no longer independently drift (the CLAUDE.md
* "Generative Fix Divergence" class the prior duplication risked).
@@ -568,15 +565,14 @@ function _resolveNamedConverter(converterName: string, kindLabel: string): (...a
* `tests/agent-descriptor-parity.test.cjs` proves byte-identical output
* between the inline loop and a SYNTHETIC descriptor registry (the same
* override seam `resolveRuntimeArtifactLayoutFromRegistry` exposes) for all
* six runtimes the inline loop still served: claude, cline, codex, hermes,
* kilo, opencode.
* runtimes the inline loop still served (claude, codex, opencode).
*
* Both findings the prior revision of this comment named as STILL deferred
* are now CLOSED (#2875 Part 2 Task A/B/C), measured against the real
* `capability.json` entries and the real production entry points, not
* argued from this module alone:
*
* 1. **kilo/opencode reaching `layout.kinds`.** `installEngine.
* 1. **opencode reaching `layout.kinds`.** `installEngine.
* installAgentsKindStandalone` (install-engine.cts) is called from inside
* `installOpencodeFamilyArtifacts` and resolves the agents kind through
* THIS SAME `resolveRuntimeArtifactLayout`/`convertedAgentsKind` path —
@@ -589,18 +585,15 @@ function _resolveNamedConverter(converterName: string, kindLabel: string): (...a
* install-tree golden fixture (`tests/fixtures/install-tree/claude-local.json`)
* is what caught the gap when it was first missed.
* 2. **`/msd:surface` / `applySurface` activation.** Confirmed convergent,
* not merely non-broken: for all six runtimes (claude, cline, codex,
* hermes, kilo, opencode), staging via `applySurface` into a freshly
* not merely non-broken: for every runtime (claude, codex, opencode),
* staging via `applySurface` into a freshly
* wiped `agents/` directory produces byte-identical output (including
* filenames) to `installRuntimeArtifacts`'s own write — verified directly
* against the built registry, not inferred.
*
* The inline loop (`_DESCRIPTOR_AGENTS_RUNTIMES` and the `bin/install.js`
* agent-staging block it gated) is DELETED — every runtime the registry
* declares an `agents` kind for is descriptor-driven now, including a
* seventh runtime (`kimi-code`) this comment's own prior measurement missed
* (it fell through the inline loop's generic `else if` branch, same as
* claude, with no dedicated dialect arm — caught by the same golden fixture).
* declares an `agents` kind for is descriptor-driven now.
*
* Codex's `config.toml [agents.msd-*]` strip (`bin/install.js`, under
* `isMinimalMode` + `hostBehaviors.tomlConfigInstall`) remains the one
@@ -636,16 +629,16 @@ function convertedAgentsKind(
const rawConverter = _resolveNamedConverter(converterName, 'agents') as
(content: string, arg2?: boolean | { isAgent?: boolean; modelOverride?: string | null; variant?: string | null }) => string;
// #2875 Part 2 (J5-J8): kilo/opencode agent converters take an options
// #2875 Part 2 (J5-J8): the opencode agent converter takes an options
// bag (`{isAgent, modelOverride}`), not the `isGlobal` boolean every
// other agent converter's 2nd positional arg means — mirrors the
// inline loop's per-runtime `frontmatterDialect === 'opencode' | 'kilo'`
// branches (bin/install.js), which resolve model_overrides[agent] >
// inline loop's per-runtime `frontmatterDialect === 'opencode'`
// branch (bin/install.js), which resolves model_overrides[agent] >
// model_profile_overrides.<runtime>.<tier> > omit BEFORE calling the
// converter. Resolved ONCE per stage() call (not per file — a pure
// function of configDir/targetDir) via the single shared precedence
// resolver so kilo and opencode can never diverge (J8).
const needsModelOverride = converterName === 'convertClaudeToOpencodeFrontmatter' || converterName === 'convertClaudeToKiloFrontmatter';
// resolver (J8).
const needsModelOverride = converterName === 'convertClaudeToOpencodeFrontmatter';
let converter: (content: string, isGlobal?: boolean, meta?: { agentName: string }) => string;
if (needsModelOverride) {
const overrideTargetDir = agentCtx?.targetDir ?? configDir;
@@ -665,10 +658,7 @@ function convertedAgentsKind(
// the risk this gate avoids. #1156's rule for `model: inherit` is the
// precedent: do not emit a key the runtime may not understand.
//
// OpenCode only; the kilo converter ignores the field (no EFFORT_ARGV.kilo).
const effortConfig = converterName === 'convertClaudeToOpencodeFrontmatter'
? installEffortResolver.readMsdEffectiveEffortConfig(overrideTargetDir)
: null;
const effortConfig = installEffortResolver.readMsdEffectiveEffortConfig(overrideTargetDir);
converter = (content, _isGlobal, meta) => {
const modelOverride = meta
? installModelOverrideResolver.resolveAgentModelOverride(meta.agentName, modelOverrides, runtimeResolver)
@@ -687,7 +677,7 @@ function convertedAgentsKind(
return rawConverter(content, { isAgent: true, modelOverride, variant });
};
} else {
// isGlobal is threaded so scope-aware agent converters (copilot, antigravity)
// isGlobal is threaded so scope-aware agent converters (antigravity)
// choose global-home vs workspace-relative paths; converters that only take
// (content) ignore the extra positional arg. Mirrors skillsKind's scope
// threading (#1173).
@@ -707,68 +697,17 @@ function convertedAgentsKind(
};
}
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string, sourceContext: SourceResolutionContext): ArtifactKind {
return {
kind: 'kimi-agents',
destSubpath,
prefix,
stage: (resolved, agentCtx) => {
const buildKimiAgentArtifacts = conversionExports['buildKimiAgentArtifacts'] as (opts: {
rootAgent?: string;
subagents?: Array<{ path: string; content: string }>;
}) => {
root: { yaml: string; prompt: string };
subagents: Array<{ name: string; yaml: string; prompt: string }>;
};
// #2995: compose at staging (identity converter) so the readFileSync below
// sees marker-free content — same single composing stager as agentsKind.
const stagedAgents = stageAgentsForRuntimeWithConverter(
sourceRootFor(sourceContext, 'agents'),
resolved,
(content: string) => content,
false,
agentCtx,
);
const subagents: Array<{ path: string; content: string }> = [];
if (installFs().existsSync(stagedAgents)) {
for (const entry of installFs().readdirSync(stagedAgents, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
const agentPath = path.join(stagedAgents, entry.name);
subagents.push({
path: posixNormalize(path.join('agents', entry.name)),
content: installFs().readFileSync(agentPath, 'utf8'),
});
}
}
const rootAgent = `---\nname: msd\ndescription: Run MSD workflows in Kimi CLI.\ntools: Agent\n---\n\n# MSD for Kimi CLI\n\nCoordinate installed /skill:msd-* workflows and route work to generated MSD subagents when a workflow requires an agent handoff.\n`;
const artifacts = buildKimiAgentArtifacts({ rootAgent, subagents });
const stageDir = mkInstallTempDir('msd-kimi-agents-');
installProfiles.STAGED_DIRS.add(stageDir);
installFs().writeFileSync(path.join(stageDir, 'msd.yaml'), artifacts.root.yaml);
installFs().writeFileSync(path.join(stageDir, 'msd.md'), artifacts.root.prompt);
const subagentsDir = path.join(stageDir, 'subagents');
installFs().mkdirSync(subagentsDir, { recursive: true });
for (const artifact of artifacts.subagents) {
installFs().writeFileSync(path.join(subagentsDir, `${artifact.name}.yaml`), artifact.yaml);
installFs().writeFileSync(path.join(subagentsDir, `${artifact.name}.md`), artifact.prompt);
}
return stageDir;
},
};
}
/**
* Build a skills kind descriptor.
*
* @param destSubpath
* @param prefix
* @param converterName name of converter function in Runtime Artifact Conversion exports
* @param runtime canonical runtime ID (gates Hermes/Qwen branding in converter)
* @param runtime canonical runtime ID (passed through to the converter)
* @param configDir runtime config dir (for .msd-source marker resolution)
* @param nested if true, nest concrete skills under their ns-* routers (#69)
* @param scope install scope; converted to isGlobal and passed as 5th positional
* arg so scope-aware converters (antigravity, copilot) can choose
* arg so scope-aware converters (antigravity) can choose
* between global home paths and workspace-relative paths without
* colliding with the `runtime` string at position 3.
* @param capabilityRegistry #2322: optional capability registry — captured in the
@@ -797,8 +736,8 @@ function skillsKind(
// Compute cmdNames once per stage call for performance (#3583).
// Extra trailing args are ignored by converters that don't need them. The
// isGlobal flag is the 5th positional (NOT the 3rd): the 3rd positional is
// `runtime` for the claude/kimi/cline converters, so the scope-aware
// converters (antigravity, copilot) read isGlobal from position 5 to avoid
// `runtime` for the claude converter, so the scope-aware
// converters (antigravity) read isGlobal from position 5 to avoid
// colliding with `runtime` and always taking the global branch.
const cmdNames = conversionExports.readMsdCommandNames
? conversionExports.readMsdCommandNames()
@@ -874,17 +813,10 @@ function convertedCommandsKind(
// flat conservatively. Verified June 2026:
//
// NEST (confirmed non-recursive / one-level scan):
// cline — cline/cline skills.ts scanSkillsDirectory uses flat fs.readdir
// qwen — QwenLM/qwen-code skill-load.ts flat readdir ("depth 2 enough")
// hermes — hermes-agent.nousresearch.com/docs/user-guide/features/skills
// (single-level subdir probe of the tap path)
// augment — https://docs.augmentcode.com/cli/skills (flat single-level)
// trae — docs.trae.ai/ide/skills + Trae-AI/TRAE#2253 (flat; nesting errors)
// Trae IDE (trae.ai), not trae-agent — see runtime-homes.cts header note
// (none of the currently supported runtimes)
// FLAT (recursive loader → nesting gives no saving):
// cursor — https://cursor.com/docs/skills (walks skills root recursively)
// opencode — sst/opencode skill/index.ts glob "skills/**/SKILL.md"
// kilo — Kilo-Org/kilocode (opencode fork, same ** glob)
//
// FLAT (one-level scan, but concrete skills must be directly discoverable):
// antigravity— https://antigravity.google/docs/skills + /docs/cli-plugins
@@ -899,9 +831,6 @@ function convertedCommandsKind(
//
// FLAT (nested-scan behaviour unconfirmed → conservative):
// codex — developers.openai.com/codex/skills/
// copilot — docs.github.com/en/copilot/concepts/agents/about-agent-skills
// windsurf — docs.devin.ai/desktop/cascade/skills
// codebuddy — codebuddy.ai/docs/cli/skills
// ---------------------------------------------------------------------------
// Descriptor-driven dispatch helpers (ADR-857 phase 5d)
@@ -967,10 +896,6 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
result = skillsKind(destSubpath, prefix, converter, runtime, configDir, nested, scope, sourceContext, capabilityRegistry);
break;
case 'kimi-agents':
result = kimiAgentsKind(destSubpath, prefix, configDir, sourceContext);
break;
default:
throw new TypeError(
`resolveRuntimeArtifactLayout: unknown kind '${kind}' in descriptor for runtime '${runtime}'`,
@@ -1060,12 +985,12 @@ function resolveRuntimeArtifactLayoutFromRegistry(
// rather than "where does a file land". A new function, not a widened
// signature — see .msd/phase/feat-2871-trigger-resolution/40-design.md.
//
// Only `commands` and `skills` are trigger-bearing. `agents` / `kimi-agents`
// Only `commands` and `skills` are trigger-bearing. `agents`
// are a SEPARATE dispatch interface point (subagent invocation via
// `subagent_type` / named dispatch, never a `/msd-<name>` a user types) — see
// 40-design.md's "agents are not trigger-bearing" correction to ADR-2866.
// Excluding them here is deliberate, not an oversight: including `agents`
// would misreport windsurf (whose global scope emits agents only) as fully
// would misreport a runtime whose global scope emits agents only as fully
// shadowing its local `/msd-*` surface, when in fact nothing shadows it.
/** The trigger-bearing subset of ArtifactKindName — mirrors
@@ -1275,7 +1200,7 @@ function resolveTriggerSurface(runtime: string, scopes: InstallScope[], opts: Tr
if (!scopeSet.has(scope)) continue;
const entries = layout[scope] ?? [];
for (const entry of entries) {
if (entry.kind !== 'commands' && entry.kind !== 'skills') continue; // excludes agents/kimi-agents
if (entry.kind !== 'commands' && entry.kind !== 'skills') continue; // excludes agents
const kind = entry.kind;
const destSubpath = posixNormalize(entry.destSubpath);
const namespacedByDir = isNamespacedByDir(kind, entry.destSubpath, entry.prefix);