feat(#2093): migrate Kilo onto EoS imperative adapter + hook-bus/model/MCP/dispatch upgrades (ADR-1239)

Fold remaining isKilo logic branches into descriptor-driven reads:
finishPermissionWriter (uninstall cleanup), skipSharedHooksInstall (hooks
copy), and a skills converter-name registry (the artifactLayout.converter
field is now load-bearing, not decorative). frontmatterDialect stays the
documented dispatch key for frontmatter (no descriptor field for it). Dead
isKilo destructure bindings removed. Byte-identical golden parity for all 16
runtimes (opencode, which shares kilo's combined-family path, verified clean).

UPGRADE 1 (hook bus): install .kilo/plugins/gsd-core.js native plugin +
extensionEvents:"kilo" + EXTENSION_EVENT_SURFACES.kilo (OpenCode-fork bus).
UPGRADE 2 (active model): populate runtimeTierDefaults.kilo + thread
modelOverride through convertClaudeToKiloFrontmatter — model no longer stripped
from agents. UPGRADE 3 (MCP): document the gsd-core MCP companion under kilo's
mcp config key. UPGRADE 4 (named dispatch): agents/*.md mode:subagent roster is
the Task-tool dispatch surface (tested); subagentToolkit stays 'undocumented'
per AC so dispatch degrades to 'degraded' by design.

Model-catalog single-source edit ripples the shared model-catalog.json hash
into all 16 golden fixtures (expected). Inline defect fixes (no-defer): stale-
bake-guard resolveAgentDir 'agent'->'agents' (was a silent no-op for opencode/
codex), hardcoded 'Removed OpenCode plugin' uninstall log -> generic, and the
connect-gsd-mcp-server.md OpenCode mcpServers->mcp doc error.

Tests: kilo-imperative-reference (adapter/axes/fail-closed/degradation/
hostBehaviors + widened isKilo source-grep across 4 modules) + kilo-upgrades
(plugin parity+load, model-override converter, agents dispatch surface, MCP
doc). Matrix + how-to + config docs updated; changeset added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-10 17:40:50 -04:00
parent 61d04daacf
commit f014ec83bd
41 changed files with 1666 additions and 117 deletions

View File

@@ -1548,6 +1548,7 @@ const capabilities = {
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"extensionEvents": "kilo",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "settings-json",
@@ -1577,7 +1578,13 @@ const capabilities = {
"flatCommandDir": "command",
"combinedFamilyInstall": true,
"frontmatterDialect": "kilo",
"skipUpdateBannerCommand": true
"nativePlugin": {
"dir": "plugins",
"file": "gsd-core.js",
"source": ".kilo/plugins/gsd-core.js"
},
"skipUpdateBannerCommand": true,
"skipSharedHooksInstall": true
}
}
},
@@ -4669,6 +4676,7 @@ const runtimes = {
},
"commandStyle": "slash-hyphen",
"hooksSurface": "none",
"extensionEvents": "kilo",
"sandboxTier": "none",
"supportTier": 2,
"installSurface": "settings-json",
@@ -4698,7 +4706,13 @@ const runtimes = {
"flatCommandDir": "command",
"combinedFamilyInstall": true,
"frontmatterDialect": "kilo",
"skipUpdateBannerCommand": true
"nativePlugin": {
"dir": "plugins",
"file": "gsd-core.js",
"source": ".kilo/plugins/gsd-core.js"
},
"skipUpdateBannerCommand": true,
"skipSharedHooksInstall": true
}
}
},

View File

@@ -711,7 +711,7 @@ const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
// DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
// plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
// host exposes no extension surface (engine owns the bus, e.g. VS Code).
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'none']);
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'none']);
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);

View File

@@ -3,9 +3,10 @@
/**
* Stale-bake guard for static-frontmatter runtimes (#1688, follow-up to #1650).
*
* Runtimes `codex` and `opencode` bake the resolved model ID into each agent's
* static config at install time (bin/install.js ~5667-5767 for codex,
* ~10008-10026 for opencode). Their task/spawn_agent interfaces do not accept
* Runtimes `codex`, `opencode`, and (since #2093) `kilo` bake the resolved
* model ID into each agent's static config at install time (bin/install.js
* ~5667-5767 for codex, ~10008-10026 for opencode, and the adjacent kilo
* branch added by #2093). Their task/spawn_agent interfaces do not accept
* an inline `model` parameter, so editing `model_overrides` in
* `.planning/config.json` or `~/.gsd/defaults.json` has NO effect until the
* user re-runs `gsd install <runtime>` (or `gsd update`). The failure is
@@ -25,9 +26,17 @@ const os = require('os');
* Runtimes whose agent config is static frontmatter/TOML baked at install time.
* MUST stay in sync with the bake paths in bin/install.js. The parity test in
* tests/stale-bake-guard.test.cjs asserts this matches the runtimes that
* actually emit a baked model: line id #2256 (opencode) and #49/#2256 (codex).
* actually emit a baked model: line id #2256 (opencode), #49/#2256 (codex),
* and #2093 (kilo).
*/
const STATIC_FRONTMATTER_RUNTIMES = Object.freeze(['codex', 'opencode']);
const STATIC_FRONTMATTER_RUNTIMES = Object.freeze(['codex', 'kilo', 'opencode']);
/** Per-runtime `gsd install` flag, for the remediation hint in the warning. */
const INSTALL_FLAG_BY_RUNTIME = Object.freeze({
codex: '--codex',
opencode: '--opencode',
kilo: '--kilo',
});
const _warnedKeys = new Set();
@@ -55,7 +64,7 @@ function formatStaleBakeWarning({ runtime, configPath, configMtimeMs, agentMtime
const signal = detectStaleBake({ runtime, configMtimeMs, agentMtimeMs });
if (!signal) return '';
const configDate = new Date(configMtimeMs).toISOString();
const installFlag = runtime === 'opencode' ? '--opencode' : '--codex';
const installFlag = INSTALL_FLAG_BY_RUNTIME[runtime] || `--${runtime}`;
return [
`gsd: model config in ${configPath} changed since agents were last baked (${configDate}).`,
` Static-frontmatter runtime '${runtime}' ignores the new model_overrides`,
@@ -79,13 +88,24 @@ function resolveRuntimeFromConfig(config) {
/**
* Resolve the install root for a runtime's agent files, honoring the same env
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR). Returns the
* absolute directory or `null` for unsupported runtimes.
* vars the installer does (CODEX_HOME, OPENCODE_CONFIG_DIR, KILO_CONFIG_DIR).
* Returns the absolute directory or `null` for unsupported runtimes.
*/
function resolveAgentDir(runtime, { env = process.env, homedir = os.homedir } = {}) {
if (runtime === 'opencode') {
const base = (env.OPENCODE_CONFIG_DIR && String(env.OPENCODE_CONFIG_DIR).trim()) || path.join(homedir(), '.config', 'opencode');
return path.join(base, 'agent');
// #2093 fix: the installer writes agents to `<base>/agents` (plural — see
// bin/install.js's universal `agentsDest = path.join(targetDir, 'agents')`,
// verified against a live `--opencode --global` install). The prior
// singular `agent` never matched the real install output, so this guard's
// `findOldestAgentMtime` always hit ENOENT and warnIfStaleBake was a
// silent no-op for opencode in production — discovered while wiring the
// parallel kilo entry below (same bake mechanism, #2093).
return path.join(base, 'agents');
}
if (runtime === 'kilo') {
const base = (env.KILO_CONFIG_DIR && String(env.KILO_CONFIG_DIR).trim()) || path.join(homedir(), '.config', 'kilo');
return path.join(base, 'agents');
}
if (runtime === 'codex') {
const base = (env.CODEX_HOME && String(env.CODEX_HOME).trim()) || path.join(homedir(), '.codex');
@@ -148,7 +168,7 @@ function findOldestAgentMtime(runtime, { env = process.env, homedir = os.homedir
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith('gsd-')) continue;
const isAgentFile = (runtime === 'opencode' && entry.name.endsWith('.md'))
const isAgentFile = ((runtime === 'opencode' || runtime === 'kilo') && entry.name.endsWith('.md'))
|| (runtime === 'codex' && (entry.name.endsWith('.toml') || entry.name.endsWith('.md')));
if (!isAgentFile) continue;
try {

View File

@@ -38,9 +38,9 @@
"haiku": { "model": "anthropic/claude-haiku-4-5" }
},
"kilo": {
"opus": null,
"sonnet": null,
"haiku": null
"opus": { "model": "anthropic/claude-opus-4-8" },
"sonnet": { "model": "anthropic/claude-sonnet-5" },
"haiku": { "model": "anthropic/claude-haiku-4-5" }
},
"cline": {
"opus": null,

View File

@@ -360,7 +360,8 @@ Built-in tier defaults by runtime:
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` |
| Group B (`kilo`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | |
| `kilo` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` |
| Group B (`cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | |
Display a table to the user showing the effective configuration:
@@ -387,8 +388,8 @@ AskUserQuestion([
options: [
{ label: "Keep current (<runtime>)", description: "Configure overrides for the current runtime." },
{ label: "Common runtimes", description: "claude, codex, gemini, qwen" },
{ label: "Additional runtimes", description: "opencode, copilot, hermes" },
{ label: "Other (Group B or custom)", description: "kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity, or a custom runtime string." }
{ label: "Additional runtimes", description: "opencode, copilot, hermes, kilo" },
{ label: "Other (Group B or custom)", description: "cline, cursor, windsurf, augment, trae, codebuddy, antigravity, or a custom runtime string." }
]
}
])
@@ -423,7 +424,8 @@ AskUserQuestion([
options: [
{ label: "opencode", description: "OpenCode (uses anthropic/ prefix)." },
{ label: "copilot", description: "GitHub Copilot." },
{ label: "hermes", description: "Hermes (uses anthropic/ prefix)." }
{ label: "hermes", description: "Hermes (uses anthropic/ prefix)." },
{ label: "kilo", description: "Kilo Code (uses anthropic/ prefix)." }
]
}
])