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

@@ -191,7 +191,7 @@ function provisionRuntimeSurfaceCorpus(
if (isGlobalScope(scope as InstallScope)) {
for (const kind of layout.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');
}
}
if (required.size === 0) return;
@@ -283,7 +283,6 @@ function applyOpencodeFamilyPathPrefix(content: string, runtime: string, pathPre
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
content = content.replace(/~\/\.opencode\//g, pathPrefix);
content = content.replace(/~\/\.kilo\//g, pathPrefix);
return content;
}
@@ -297,25 +296,15 @@ function convertClaudeCommandToOpencodeSkill(content: string, skillName: string)
return (runtimeArtifactConversion as any).convertClaudeCommandToOpencodeSkill(content, skillName);
}
/**
* Convert a Claude command (.md) to a Kilo skill (SKILL.md).
* Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema).
*/
function convertClaudeCommandToKiloSkill(content: string, skillName: string): string {
return (runtimeArtifactConversion as any).convertClaudeCommandToKiloSkill(content, skillName);
}
/**
* Converter-name registry for the OpenCode-family combined skills installer
* (ADR-1239 / #2093). Maps the `converter` string declared on each runtime's
* artifactLayout skills-kind descriptor (capabilities/<runtime>/capability.json)
* to the actual conversion function, so `installOpencodeFamilySkills` dispatches
* off the descriptor instead of a `frontmatterDialect === 'kilo'` runtime check.
* off the descriptor instead of a `frontmatterDialect` runtime check.
*/
const SKILLS_CONVERTER_REGISTRY: Record<string, (content: string, skillName: string) => string> = {
convertClaudeCommandToOpencodeSkill,
convertClaudeCommandToKiloSkill,
convertClaudeCommandToKimiCodeSkill: runtimeArtifactConversion.convertClaudeCommandToKimiCodeSkill,
};
// ---------------------------------------------------------------------------
@@ -558,7 +547,7 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
* Migrate a legacy dev-preferences.md (saved from commands/msd/) into the
* runtime-aware SKILL.md location used by the writer after #2973.
*
* For runtimes with a nested skills layout (e.g. Hermes: skills/msd/<stem>/),
* For runtimes with a nested skills layout (skills/msd/<stem>/),
* the target is <configDir>/skills/msd/dev-preferences/SKILL.md.
* For runtimes with a flat skills layout (prefix='msd-'), the target is
* <configDir>/skills/msd-dev-preferences/SKILL.md.
@@ -573,7 +562,7 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
* user-artifact-staging.cts staged batch's disk contents (#2875) — every
* call site reads this back AFTER its own wipe, never held in memory
* across it.
* @param runtime - canonical runtime ID (e.g. 'hermes', 'qwen', 'claude')
* @param runtime - canonical runtime ID (e.g. 'claude')
* @param scope - install scope
* @returns true if a file was migrated, false otherwise
*/
@@ -618,7 +607,7 @@ function _resolveDevPreferencesSkillTarget(targetDir: string, runtime?: string,
if (runtime) {
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as any);
const skillsKindEntry = layout.kinds.find((k: any) => k.kind === 'skills');
if (!skillsKindEntry) return null; // runtime has no skills layout at this scope (e.g. cline local)
if (!skillsKindEntry) return null; // runtime has no skills layout at this scope
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'msd-dev-preferences';
// #2911: same destination-root defect as _copyStaged/applySurface — honor
// skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
@@ -652,7 +641,7 @@ function migrateLegacyDevPreferencesToSkill(
): boolean {
if (!saved || !saved.has('dev-preferences.md')) return false;
const target = _resolveDevPreferencesSkillTarget(targetDir, runtime, scope);
if (!target) return false; // runtime has no skills layout at this scope (e.g. cline local)
if (!target) return false; // runtime has no skills layout at this scope
// #3712 — the SIXTH writer that resolves a skills-kind `home` override.
// Exported and directly callable, and `_runLegacyInstallMigrations` runs it
// BEFORE installRuntimeArtifacts' own assertion, so a future runtime pairing a
@@ -728,7 +717,6 @@ function migrateLegacyDevPreferencesToSkill(
* encodes the MSD namespace as its last segment (e.g. `commands/msd`), in
* which case write as `${stem}.md` (directory IS the namespace).
* - agents: write as-is (files already carry their own `msd-` prefix).
* For kimi-agents kind: recursively copy generated YAML/prompt files.
*/
function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: string, runtime?: string): void {
// Defense-in-depth: verify destDir is within the install root even if the
@@ -775,11 +763,6 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
return;
}
if (kind.kind === 'kimi-agents') {
installFs().cpSync(stagedDir, destDir, { recursive: true });
return;
}
// commands or agents
const entries = installFs().readdirSync(stagedDir, { withFileTypes: true });
// For commands: apply prefix unless the destSubpath's last segment already
@@ -806,10 +789,9 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
let destName: string;
if (kind.kind === 'agents') {
// Agent files already carry the msd- prefix in the source dir.
// #2099: descriptor-driven via hostBehaviors.agentFileExtension (was
// hardcoded `runtime === 'copilot'`). copilot declares '.agent.md';
// every other runtime's descriptor leaves this unset, so destName falls
// back to entry.name unchanged (byte-parity, #1575 origin comment).
// #2099: descriptor-driven via hostBehaviors.agentFileExtension. No
// current runtime declares one, so destName falls back to entry.name
// unchanged (byte-parity, #1575 origin comment).
const _agentExt = runtime ? _hostBehaviors(runtime).agentFileExtension : undefined;
destName = _agentExt
? entry.name.replace(/\.md$/, _agentExt)
@@ -835,28 +817,13 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
/**
* Remove MSD-prefixed entries from destDir matching kind.prefix.
* For the prefix='' case: the destSubpath IS the namespace — remove the entire
* destDir. (No current runtime uses prefix='' after #947 reversed Hermes; kept
* as a defensive guard for future runtimes.)
* destDir. (No current runtime uses prefix=''; kept as a defensive guard for
* future runtimes.)
*/
function _removeMsdEntries(destDir: string, kind: any): void {
if (!installFs().existsSync(destDir)) return;
if (kind.kind === 'kimi-agents') {
for (const fileName of ['msd.yaml', 'msd.md']) {
installFs().rmSync(path.join(destDir, fileName), { force: true });
}
const subagentsDir = path.join(destDir, 'subagents');
if (installFs().existsSync(subagentsDir)) {
for (const entry of installFs().readdirSync(subagentsDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith('msd-')) continue;
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue;
installFs().rmSync(path.join(subagentsDir, entry.name), { force: true });
}
}
return;
}
if (kind.prefix === '') {
// Whole-namespace removal (Hermes nested case — destSubpath is skills/msd)
// Whole-namespace removal (nested case — destSubpath is skills/msd)
// The directory itself is the MSD namespace, so remove it entirely.
installFs().rmSync(destDir, { recursive: true, force: true });
return;
@@ -901,37 +868,6 @@ function _restoreDir(dir: string, snapshot: Map<string, Buffer>): void {
}
}
// ---------------------------------------------------------------------------
// _removeHermesBareStemDirs
// ---------------------------------------------------------------------------
/**
* After the layout-driven install loop writes new msd-<stem>/ dirs to
* skills/msd/, remove any pre-existing bare-stem dirs (skills/msd/<stem>/)
* that correspond to the newly installed msd-<stem> entries.
*
* @param nestedMsdDir absolute path to skills/msd/ category dir
*/
function _removeHermesBareStemDirs(nestedMsdDir: string): void {
if (!installFs().existsSync(nestedMsdDir)) return;
const entries = installFs().readdirSync(nestedMsdDir, { withFileTypes: true });
// Collect the set of stems that were installed as msd-<stem>/ this run.
const installedStems = new Set<string>();
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
installedStems.add(entry.name.slice('msd-'.length)); // e.g. 'quick', 'dev-preferences'
}
}
// Remove any bare <stem>/ dir for which msd-<stem>/ was just installed.
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('msd-') && installedStems.has(entry.name)) {
installFs().rmSync(path.join(nestedMsdDir, entry.name), { recursive: true });
}
}
}
// ---------------------------------------------------------------------------
// Legacy migration helpers
// ---------------------------------------------------------------------------
@@ -947,10 +883,8 @@ function _removeHermesBareStemDirs(nestedMsdDir: string): void {
function _runLegacyInstallMigrations(runtime: string, configDir: string, scope: string = 'global'): void {
const legacyCommandsMsd = path.join(configDir, 'commands', 'msd');
// Claude / Qwen / Hermes: clean up legacy commands/msd/ and preserve dev-preferences
// for migration. The actual migration call is deferred to after all layout cleanup so
// that for Hermes the flat skills/msd-*/ removal (below) does not delete the freshly
// created skills/msd-dev-preferences/ skill dir.
// Claude: clean up legacy commands/msd/ and preserve dev-preferences for
// migration. The actual migration call is deferred to after all layout cleanup.
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
if (_hostBehaviors(runtime).legacyCommandsMsdInstallMigration) {
if (installFs().existsSync(legacyCommandsMsd)) {
@@ -967,37 +901,16 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
const stagingRoot = _tryResolveUserArtifactStagingRoot(configDir);
if (stagingRoot !== null) {
// #2875 (#1874-F19): staged DURABLY to disk before the wipe below, so a
// crash anywhere in this function — including the Hermes flat-skills
// wipe further down, previously inside the same in-memory-only window
// — survives via recoverOrphanedUserArtifacts on the next run.
// crash anywhere in this function survives via
// recoverOrphanedUserArtifacts on the next run.
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsMsd, ['dev-preferences.md'], stagingRoot);
installFs().rmSync(legacyCommandsMsd, { recursive: true });
}
}
}
// Hermes: remove pre-#2841 flat skills/msd-*/ entries that lived alongside
// the new skills/msd/ nested layout.
if (runtime === 'hermes') {
const flatSkillsDir = path.join(configDir, 'skills');
if (installFs().existsSync(flatSkillsDir)) {
for (const entry of installFs().readdirSync(flatSkillsDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
installFs().rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
}
}
}
// Hermes: bare-stem skills/msd/<stem>/ cleanup is deferred to AFTER the
// layout-driven install loop in installRuntimeArtifacts, where the exact set
// of staged msd-<stem>/ dirs is known. Removing here (before staging) would
// require readMsdCommandNames() which misses skills like 'dev-preferences'
// that are not in the commands directory. See _removeHermesBareStemDirs().
}
// Migrate dev-preferences.md content → runtime-aware SKILL.md location (#2973).
// Done after all layout cleanup so Hermes flat-dir removal does not delete the
// newly created skill dir. No-op if skill file already exists.
// Done after all layout cleanup. No-op if skill file already exists.
if (stagedLegacyArtifacts) {
// #2875: read the content back from the DISK-staged copy (fresh, after
// every wipe above has already run) rather than an in-memory value held
@@ -1095,7 +1008,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
* @returns staged legacy artifacts for post-removal migration, or null
*/
function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: string = 'global'): ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null {
// commands/msd/ is a legacy location for Qwen, Hermes, and all Claude installs.
// commands/msd/ is a legacy location for all Claude installs.
// Prior to #1367 fix, Claude-local used commands/msd/<cmd>.md (colon-namespaced).
// After #1367, Claude-local uses flat commands/msd-<cmd>.md. The inline uninstall
// block (1c) handles removal of flat files; this function handles the legacy
@@ -1114,7 +1027,7 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
// module does (ambient default: real fs here, since this call is never
// wrapped in withInstallFs).
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
// commands/msd/ is a legacy location for Qwen, Hermes, and Claude global.
// commands/msd/ is a legacy location for Claude global.
// Claude local is intentionally excluded: the inline uninstall block (1c) handles
// commands/msd/ for claude local, preserving dev-preferences.md by restoring it
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
@@ -1145,30 +1058,6 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
}
}
// Hermes: pre-#2841 flat skills/msd-*/ entries
if (runtime === 'hermes') {
const flatSkillsDir = path.join(configDir, 'skills');
if (fs.existsSync(flatSkillsDir)) {
for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
}
}
}
// Hermes: pre-#947 bare-stem skills/msd/<stem>/ entries (dirs that do NOT
// start with 'msd-') — the #3664 layout used prefix='' so MSD-owned skills
// had bare names (e.g. skills/msd/help/). These are stale on uninstall.
const nestedMsdDirForUninstall = path.join(configDir, 'skills', 'msd');
if (fs.existsSync(nestedMsdDirForUninstall)) {
for (const entry of fs.readdirSync(nestedMsdDirForUninstall, { withFileTypes: true })) {
if (entry.isDirectory() && !entry.name.startsWith('msd-')) {
fs.rmSync(path.join(nestedMsdDirForUninstall, entry.name), { recursive: true });
}
}
}
}
// Return staged artifacts so the caller can migrate after layout-driven removal.
return stagedLegacyArtifacts;
}
@@ -1226,14 +1115,14 @@ function installRuntimeArtifacts(
// before materializing the current layout (#2644).
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
// Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
// Combined-family runtimes (OpenCode, ADR-1239 / #2087): route through
// the dedicated combined commands+skills+plugin orchestrator instead of the
// generic layout-driven loop below, mirroring the bespoke install path that
// previously lived inline in bin/install.js.
const behaviors = _hostBehaviors(runtime);
const projectDir = scope === 'global' ? process.cwd() : configDir;
if (behaviors.combinedFamilyInstall) {
// #2329: combined-family runtimes (OpenCode/Kilo) bypass
// #2329: combined-family runtimes (OpenCode) bypass
// _runLegacyInstallMigrations below entirely (early return), so their
// legacy-directory cleanup needs its own pre-materialization hook here.
_migrateLegacyOpencodeCommandDir(runtime, configDir, behaviors);
@@ -1377,7 +1266,7 @@ function installRuntimeArtifacts(
// then restore after. This preserves user dirs across a wipe-and-replace
// install (#2973 / #3664).
//
// All runtimes (incl. Hermes after #947) use prefix='msd-'.
// All runtimes use prefix='msd-'.
// _removeMsdEntries removes only msd-* entries; non-msd-* user dirs are
// untouched. Preserve the explicit user-owned MSD-prefixed skill
// msd-dev-preferences, which MSD does not reinstall from source but must
@@ -1427,27 +1316,9 @@ function installRuntimeArtifacts(
}
}
// Hermes: after the install loop has written all msd-<stem>/ dirs to
// skills/msd/, remove any stale bare-stem dirs (skills/msd/<stem>/) that
// correspond to the newly installed msd-<stem> entries. This is the robust
// replacement for the readMsdCommandNames()-based pre-install cleanup that
// missed skills like 'dev-preferences' (#947 adversarial review).
//
// We run this AFTER the install loop so the installed set is authoritative:
// every msd-<stem>/ present now was written this run (or was there before
// with the same prefix). User-owned bare dirs with no msd-<stem> counterpart
// are untouched.
let hermesBareStemCleanup = false;
if (runtime === 'hermes') {
const nestedMsdDirForCleanup = path.join(configDir, 'skills', 'msd');
_removeHermesBareStemDirs(nestedMsdDirForCleanup);
hermesBareStemCleanup = true;
}
// Generic-branch nativePlugin staging (ADR-1239 / #2102 Stage 1): runtimes
// outside the OpenCode/Kilo combined-family install (e.g. pi, whose
// artifactLayout is empty and which never sets combinedFamilyInstall) still
// need their declared hostBehaviors.nativePlugin file copied into configDir.
// outside the OpenCode combined-family install that declare a
// hostBehaviors.nativePlugin still need that file copied into configDir.
// findInstallSourceRoot resolves the repo/package root independent of
// configDir contents (marker check, then a walk-up from __dirname), so this
// is safe even when configDir has no .msd-source marker (artifactLayout: []).
@@ -1468,7 +1339,7 @@ function installRuntimeArtifacts(
scope,
kinds: executedKinds,
cleanup: cleanupResults,
postSteps: { hermesBareStemCleanup, nativePlugin: nativePluginInstalled },
postSteps: { nativePlugin: nativePluginInstalled },
};
});
}
@@ -1478,7 +1349,7 @@ function installRuntimeArtifacts(
// ---------------------------------------------------------------------------
/**
* Install the skills layout kind for an OpenCode-family runtime (OpenCode/Kilo).
* Install the skills layout kind for an OpenCode-family runtime (OpenCode).
*
* These runtimes do NOT go through installRuntimeArtifacts (their commands use a
* bespoke flattened-command writer), so this writes ONLY the skills kind
@@ -1486,7 +1357,7 @@ function installRuntimeArtifacts(
* layout-driven (uninstallRuntimeArtifacts iterates layout.kinds), so the
* skills/ dir is cleaned up automatically once the layout declares it.
*
* @param runtime - 'opencode' or 'kilo'
* @param runtime - 'opencode'
* @param targetDir - resolved runtime config directory
* @param rawCommandsDir - staged RAW Claude command dir (caller's _stageSkills output)
* @param pathPrefix - computed config-path prefix for body rewrites
@@ -1499,7 +1370,7 @@ function installRuntimeArtifacts(
* (capabilityClusters view). When present, installed third-party capability
* skills bound to their declaring capId are unioned into the staged output —
* the actual #2322 seam (install-profiles.cts stageSkillsForRuntimeAsSkills)
* this bespoke OpenCode/Kilo writer never called. Absent -> no third-party
* this bespoke OpenCode writer never called. Absent -> no third-party
* skills staged (fail closed), matching the seam's own optional-registry
* contract.
* @returns number of msd-* skill directories written
@@ -1518,7 +1389,7 @@ function installOpencodeFamilySkills(
if (!skillsKindEntry) return 0;
// #3712: combined-family runtimes take installRuntimeArtifacts' early return
// BEFORE its guard runs, and this writer honors `skillsKindEntry.home` below and
// then prunes that destination. opencode/kilo declare no `home` today, so there
// then prunes that destination. opencode declares no `home` today, so there
// is no live escape — but that makes this a bypass waiting on a descriptor
// change rather than a safe omission, so it is guarded at the writer instead.
// Scoped to the SKILLS kind alone, for the same reason as the agents writer.
@@ -1528,7 +1399,7 @@ function installOpencodeFamilySkills(
// #2093: descriptor-driven — dispatch off the skills-kind entry's `converter`
// string (capabilities/<runtime>/capability.json artifactLayout) via the
// SKILLS_CONVERTER_REGISTRY, instead of a `frontmatterDialect === 'kilo'`
// SKILLS_CONVERTER_REGISTRY, instead of a `frontmatterDialect`
// runtime check. Fail loud if the descriptor names an unregistered converter
// (mirrors the converter=null throw in runtime-artifact-layout.cts).
const converterName: string | undefined = skillsKindEntry.converter;
@@ -1542,8 +1413,8 @@ function installOpencodeFamilySkills(
// #2911: same destination-root defect as _copyStaged/migrateLegacyDevPreferencesToSkill
// — honor skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
// -> $HOME/.agents) instead of always resolving against targetDir, so this bespoke
// OpenCode/Kilo writer lands in the SAME tree the installer and surface-apply use.
// Runtimes with no `home` override (opencode, kilo today) are unaffected. Must stay
// OpenCode writer lands in the SAME tree the installer and surface-apply use.
// Runtimes with no `home` override (opencode today) are unaffected. Must stay
// in lockstep with the sibling writers — the destination-parity test enforces it.
const installRoot: string = skillsKindEntry.home ?? targetDir;
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
@@ -1659,7 +1530,7 @@ function installOpencodeFamilySkills(
* combination that never reaches that loop's own `layout.kinds` iteration.
* Two such call sites exist (#2875 Part 2):
*
* 1. **OpenCode-family runtimes** (OpenCode/Kilo, Task A) — `hostBehaviors.
* 1. **OpenCode-family runtimes** (OpenCode, Task A) — `hostBehaviors.
* combinedFamilyInstall` makes `installRuntimeArtifacts` early-return into
* `installOpencodeFamilyArtifacts` instead, which stages commands+skills
* via its OWN bespoke writers and never called `resolveRuntimeArtifactLayout`
@@ -1679,7 +1550,7 @@ function installOpencodeFamilySkills(
* (`layout.kinds` → `agentsKindEntry.stage(resolvedProfile, agentCtx)` →
* `_copyStaged`), rather than forking a second agent-staging pipeline. A
* runtime/scope whose resolved layout declares no `agents` kind at all
* (e.g. pi, whose `artifactLayout` is empty for both scopes) is a no-op
* is a no-op
* (`null`) — mirrors `installOpencodeFamilySkills`'s own
* `if (!skillsKindEntry) return 0` contract.
*
@@ -1777,14 +1648,14 @@ function installAgentsKindStandalone(
/**
* Install the flattened commands surface for an OpenCode-family runtime
* (OpenCode/Kilo): commands/msd/**\/*.md -> command/msd-<...>.md, with
* (OpenCode): commands/msd/**\/*.md -> command/msd-<...>.md, with
* per-runtime frontmatter conversion and path-prefix/attribution rewrites.
*
* Mirrors bin/install.js's copyFlattenedCommands VERBATIM (ADR-1239 /
* #2087), except attribution is resolved via the injected
* `resolveAttribution` callback instead of a module-level getCommitAttribution.
*
* @param runtime - 'opencode' or 'kilo'
* @param runtime - 'opencode'
* @param destDir - destination directory for flattened commands (recurses with the same destDir)
* @param srcDir - source directory to walk (commands/msd/, recursing into subdirectories)
* @param pathPrefix - computed config-path prefix for body rewrites
@@ -1821,18 +1692,13 @@ function installOpencodeFamilyCommands(
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
content = processAttribution(content, resolveAttribution(runtime));
// #2093: this commands-kind entry's descriptor `converter` field is
// intentionally `null` (see capabilities/{kilo,opencode}/capability.json —
// intentionally `null` (see capabilities/opencode/capability.json —
// the flattened-command writer above applies its own path/attribution
// rewrites and has no per-file converter slot to key on), so there is no
// descriptor string to dispatch through here. `frontmatterDialect` is the
// documented, intentional dispatch key for frontmatter-shape selection —
// it is itself descriptor-driven (not a `runtime === 'kilo'` check), so it
// already satisfies the fold-to-descriptor requirement. Only the SKILLS
// converter site above (installOpencodeFamilySkills) has a real
// `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
content = _hostBehaviors(runtime).frontmatterDialect === 'kilo'
? (runtimeArtifactConversion as any).convertClaudeToKiloFrontmatter(content)
: (runtimeArtifactConversion as any).convertClaudeToOpencodeFrontmatter(content);
// descriptor string to dispatch through here. Only the SKILLS converter
// site above (installOpencodeFamilySkills) has a real `converter` string
// to key on via SKILLS_CONVERTER_REGISTRY.
content = (runtimeArtifactConversion as any).convertClaudeToOpencodeFrontmatter(content);
installFs().writeFileSync(path.join(destDir, destName), content);
}
}
@@ -1848,10 +1714,9 @@ function installOpencodeFamilyCommands(
*
* Extracted (ADR-1239 / #2102 Stage 1) from the body previously inlined in
* installOpencodeFamilyArtifacts so a runtime that is NOT part of the
* OpenCode/Kilo combined-family install (e.g. pi, whose artifactLayout is
* empty and which never sets combinedFamilyInstall) can still get its
* nativePlugin file staged via the generic installRuntimeArtifacts branch.
* Behavior for opencode/kilo is unchanged — same source resolution, same
* OpenCode combined-family install can still get its nativePlugin file
* staged via the generic installRuntimeArtifacts branch.
* Behavior for opencode is unchanged — same source resolution, same
* mkdir + copyFileSync call, same silent no-op when the source is missing.
*
* @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard)
@@ -1888,9 +1753,8 @@ function _installNativePluginIfDeclared(
// marker the installer wrote at the config root — the write that
// clobbered user-authored files. Pin it from the plugin's own directory
// instead, leaving the config root alone. The marker cannot disturb
// plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}` and pi's
// isExtensionFile() accepts only `.ts`/`.js` (see installer-migration
// 006), so a package.json here is never treated as a plugin. Never
// plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}`, so a
// package.json here is never treated as a plugin. Never
// written over a package.json MSD does not own — but when one is already
// there, say so: the adapter is CommonJS and will not load under a
// foreign `"type": "module"`, and a silent no-op would leave every guard
@@ -1922,8 +1786,8 @@ function _installNativePluginIfDeclared(
* #2329: migrate a pre-fix OpenCode install's legacy singular `command/`
* command directory into the current descriptor-driven destination (plural
* `commands/` for OpenCode — the dir OpenCode actually discovers slash
* commands from; unaffected for Kilo, whose descriptor still declares
* `command`, so `currentName === LEGACY_NAME` short-circuits below).
* commands from; a descriptor that still declares `command` short-circuits
* below via `currentName === LEGACY_NAME`).
*
* Runs BEFORE materialization writes the fresh command set to the new
* location (mirroring `_runLegacyInstallMigrations`'s ordering for the
@@ -1946,14 +1810,14 @@ function _installNativePluginIfDeclared(
* Types), so the empty-directory removal below would need this same
* hand-written glue regardless. It also intentionally is NOT reachable via
* combinedFamilyInstall's early return above `_runLegacyInstallMigrations`,
* matching the existing precedent that OpenCode/Kilo's bespoke install path
* matching the existing precedent that OpenCode's bespoke install path
* owns its own legacy cleanup rather than routing through the generic
* layout-driven migrations hook.
*/
function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, behaviors: any): void {
const LEGACY_NAME = 'command';
const currentName = behaviors.flatCommandDir || LEGACY_NAME;
if (currentName === LEGACY_NAME) return; // e.g. Kilo — legacy IS the current location; nothing to migrate
if (currentName === LEGACY_NAME) return; // legacy IS the current location; nothing to migrate
const legacyDir = path.join(configDir, LEGACY_NAME);
if (!installFs().existsSync(legacyDir)) return;
// Never follow a symlinked legacy dir out of configDir.
@@ -1998,13 +1862,13 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
// ---------------------------------------------------------------------------
/**
* Combined-family install orchestrator for OpenCode/Kilo (ADR-1239 / #2087,
* Combined-family install orchestrator for OpenCode (ADR-1239 / #2087,
* #2093). Stages the flattened commands surface + skills surface + (any
* runtime whose hostBehaviors declares `nativePlugin` — OpenCode and, since
* #2093, Kilo) native plugin adapter, mirroring the bespoke `else if (isOpencode ||
* isKilo)` block previously inlined in bin/install.js.
* runtime whose hostBehaviors declares `nativePlugin`) native plugin adapter,
* mirroring the bespoke `else if (isOpencode)` block previously inlined in
* bin/install.js.
*
* @param runtime - 'opencode' or 'kilo'
* @param runtime - 'opencode'
* @param configDir - resolved runtime config directory
* @param scope - install scope ('global' | 'local')
* @param resolvedProfile - from resolveProfile() / resolveEffectiveProfile()
@@ -2013,7 +1877,7 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
* @param capabilityRegistry - #2362: optional composed capability registry
* (capabilityClusters view), threaded straight through to
* installOpencodeFamilySkills so an installed third-party capability skill
* materializes for this combined-family (OpenCode/Kilo) install path too.
* materializes for this combined-family (OpenCode) install path too.
* Absent -> no third-party skills staged (fail closed).
* @param projectDir - project/config discovery root, distinct from configDir for global installs
* @returns #2874 design row 2: an executed-plan value, same top-level shape
@@ -2063,8 +1927,7 @@ function installOpencodeFamilyArtifacts(
// resolveRuntimeArtifactLayout's commands-kind destSubpath — a hardcoded
// literal here would silently diverge from the descriptor the moment either
// is edited (Generative Fix Divergence guard). OpenCode uses 'commands'
// (plural, the dir OpenCode actually discovers slash commands from); Kilo
// keeps its own descriptor value ('command', singular) unchanged.
// (plural, the dir OpenCode actually discovers slash commands from).
const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
configDir,
behaviors.flatCommandDir || 'command',
@@ -2093,7 +1956,7 @@ function installOpencodeFamilyArtifacts(
...(agentsResult ? [{ kind: 'agents', sourceDir: agentsResult.sourceDir, destDir: agentsResult.destDir }] : []),
],
cleanup: [],
postSteps: { hermesBareStemCleanup: false, nativePlugin: Boolean(behaviors.nativePlugin) },
postSteps: { nativePlugin: Boolean(behaviors.nativePlugin) },
};
}
@@ -2145,23 +2008,6 @@ function uninstallRuntimeArtifacts(
_removeMsdEntries(item.destDir, kind);
}
// Hermes: after removing msd-* skill dirs from skills/msd/, also remove
// the MSD-managed DESCRIPTION.md and then the category dir itself if it
// contains no user content (#947). _removeMsdEntries removed msd-* dirs
// but left the category container and DESCRIPTION.md intact.
if (runtime === 'hermes') {
const nestedMsdDir = path.join(configDir, 'skills', 'msd');
if (fs.existsSync(nestedMsdDir)) {
// Remove MSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
fs.rmSync(path.join(nestedMsdDir, 'DESCRIPTION.md'), { force: true });
// Remove the category dir if empty (no user content remaining)
const remaining = fs.readdirSync(nestedMsdDir, { withFileTypes: true });
if (remaining.length === 0) {
fs.rmSync(nestedMsdDir, { recursive: true, force: true });
}
}
}
// #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the
// runtime-aware SKILL.md location after all layout-driven removal is
// complete. Do NOT restore to commands/msd/ — the user is uninstalling.
@@ -2221,12 +2067,10 @@ export = {
migrateLegacyDevPreferencesToSkill,
applyOpencodeFamilyPathPrefix,
convertClaudeCommandToOpencodeSkill,
convertClaudeCommandToKiloSkill,
USER_OWNED_ARTIFACTS,
_runLegacyInstallMigrations,
_runLegacyUninstallCleanup,
_removeMsdEntries,
_snapshotDir,
_restoreDir,
_removeHermesBareStemDirs,
};