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:
@@ -3967,12 +3967,12 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
'opencode-subset is NOT a hookEvents value — it is the extensionEvents vocabulary (#1943)');
|
||||
});
|
||||
|
||||
test('VALID_EXTENSION_EVENTS has the extension-system dialects (opencode/pi/hermes/none — #1943/#2091)', () => {
|
||||
test('VALID_EXTENSION_EVENTS has the extension-system dialects (opencode/pi/hermes/kilo/none — #1943/#2091/#2093)', () => {
|
||||
assert.ok(VALID_EXTENSION_EVENTS instanceof Set);
|
||||
for (const v of ['opencode', 'pi', 'hermes', 'none']) {
|
||||
for (const v of ['opencode', 'pi', 'hermes', 'kilo', 'none']) {
|
||||
assert.ok(VALID_EXTENSION_EVENTS.has(v), 'VALID_EXTENSION_EVENTS must contain "' + v + '"');
|
||||
}
|
||||
assert.strictEqual(VALID_EXTENSION_EVENTS.size, 4);
|
||||
assert.strictEqual(VALID_EXTENSION_EVENTS.size, 5);
|
||||
});
|
||||
|
||||
test('VALID_SANDBOX_TIERS has exactly 2 values', () => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/scan.md": "a7fecd67e5cd655f",
|
||||
"gsd-core/workflows/secure-phase.md": "96b199dfac00e60f",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "b7050fcf24175188",
|
||||
"gsd-core/workflows/settings-advanced.md": "289b3d653d18e284",
|
||||
"gsd-core/workflows/settings-integrations.md": "4d3001ad2b5dad8f",
|
||||
"gsd-core/workflows/settings.md": "8258f7bd3700d608",
|
||||
"gsd-core/workflows/ship.md": "984bd7660e7791fd",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "77ada5fe336ed9f7",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -359,7 +359,7 @@
|
||||
"gsd-core/workflows/scan.md": "75c670d08cee8680",
|
||||
"gsd-core/workflows/secure-phase.md": "64ec4d06ca85720a",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "0c8a218bad054722",
|
||||
"gsd-core/workflows/settings-advanced.md": "a44896a18f1f2edc",
|
||||
"gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139",
|
||||
"gsd-core/workflows/settings.md": "acdd79110699a608",
|
||||
"gsd-core/workflows/ship.md": "44af1c72d86e153b",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -288,7 +288,7 @@
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "59d3c50aba8c9a6c",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "38e4e7b92b396e3c",
|
||||
"gsd-core/workflows/settings-advanced.md": "fa4a285b6bcbc384",
|
||||
"gsd-core/workflows/settings-integrations.md": "53649313d20694ae",
|
||||
"gsd-core/workflows/settings.md": "7e7458cdb2b68ec5",
|
||||
"gsd-core/workflows/ship.md": "12e8e58c077a891a",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -292,7 +292,7 @@
|
||||
"gsd-core/workflows/scan.md": "dfd92717caea0ce7",
|
||||
"gsd-core/workflows/secure-phase.md": "cf78183f06a02582",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "30dcd0980f4ae882",
|
||||
"gsd-core/workflows/settings-advanced.md": "7147ba291d13e007",
|
||||
"gsd-core/workflows/settings-integrations.md": "76eee76d6eb57657",
|
||||
"gsd-core/workflows/settings.md": "3701faed09d55247",
|
||||
"gsd-core/workflows/ship.md": "5f931a25ea9102a4",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "77ada5fe336ed9f7",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -395,7 +395,7 @@
|
||||
"gsd-core/workflows/scan.md": "1a3caa5d724d39e9",
|
||||
"gsd-core/workflows/secure-phase.md": "db91810d16964b1e",
|
||||
"gsd-core/workflows/session-report.md": "dd8fa011c9394075",
|
||||
"gsd-core/workflows/settings-advanced.md": "14de66e631f172d7",
|
||||
"gsd-core/workflows/settings-advanced.md": "38566aab0c529f2c",
|
||||
"gsd-core/workflows/settings-integrations.md": "77730321d3d6d317",
|
||||
"gsd-core/workflows/settings.md": "054c8c31b3905ced",
|
||||
"gsd-core/workflows/ship.md": "d86233c9a365effd",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -290,7 +290,7 @@
|
||||
"gsd-core/workflows/scan.md": "dcc2f76d0850e2fb",
|
||||
"gsd-core/workflows/secure-phase.md": "d87bd706f85bcad6",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "d46274308c4e2584",
|
||||
"gsd-core/workflows/settings-advanced.md": "8c88bbcb8fa0fa39",
|
||||
"gsd-core/workflows/settings-integrations.md": "a1d146d6bfd14db5",
|
||||
"gsd-core/workflows/settings.md": "f611f14f2f447f1e",
|
||||
"gsd-core/workflows/ship.md": "b9dc0aaee0ff68e7",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "c55975672c4e1895",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "6828a746997784d8",
|
||||
"gsd-core/workflows/settings-advanced.md": "93a20fc634b05964",
|
||||
"gsd-core/workflows/settings-integrations.md": "166ca51b1f33c2a3",
|
||||
"gsd-core/workflows/settings.md": "0623c673eaf04799",
|
||||
"gsd-core/workflows/ship.md": "38830806d244fe9c",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/scan.md": "b28f65d88c522767",
|
||||
"gsd-core/workflows/secure-phase.md": "f2957d4b88fb3746",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "dd44be875331ccb9",
|
||||
"gsd-core/workflows/settings-advanced.md": "7374ef968d5280f0",
|
||||
"gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5",
|
||||
"gsd-core/workflows/settings.md": "0845d073009a4619",
|
||||
"gsd-core/workflows/ship.md": "dbf8bf636cb196c0",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "e8855104c1e0417c",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "40af6774a38adb07",
|
||||
"gsd-core/workflows/settings-advanced.md": "e00cd1a5d3fbba9c",
|
||||
"gsd-core/workflows/settings-integrations.md": "3ea8095d5fad891b",
|
||||
"gsd-core/workflows/settings.md": "1925ecc2225c2216",
|
||||
"gsd-core/workflows/ship.md": "afd77be2093535f8",
|
||||
@@ -383,6 +383,7 @@
|
||||
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
|
||||
"gsd-core/workflows/verify-work.md": "9fc717845828c6e3",
|
||||
"kilo.json": "13151e97ff23c1aa",
|
||||
"plugins/gsd-core.js": "931ca839dc9eb7f1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -325,7 +325,7 @@
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "77ada5fe336ed9f7",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "ad8ebcad4626d4a8",
|
||||
"gsd-core/workflows/secure-phase.md": "e9a488cec3b4efdc",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "afa9c38b68500b29",
|
||||
"gsd-core/workflows/settings-advanced.md": "92524bd2a53b43f5",
|
||||
"gsd-core/workflows/settings-integrations.md": "d1711a95f44fdbde",
|
||||
"gsd-core/workflows/settings.md": "2e42ee34c791378a",
|
||||
"gsd-core/workflows/ship.md": "924c79e3cfd1e42b",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/scan.md": "949692db4834dd27",
|
||||
"gsd-core/workflows/secure-phase.md": "6758f1acf4113e9e",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "6f11a48034be4fa5",
|
||||
"gsd-core/workflows/settings-advanced.md": "d80d7acc4058cb40",
|
||||
"gsd-core/workflows/settings-integrations.md": "29c5de27fbbb18e9",
|
||||
"gsd-core/workflows/settings.md": "26b9b7979d3a5747",
|
||||
"gsd-core/workflows/ship.md": "9f94d0b155eb041e",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/scan.md": "63631467651d9ca8",
|
||||
"gsd-core/workflows/secure-phase.md": "6cc236e53c2e7d56",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "72d9c0759cc01d99",
|
||||
"gsd-core/workflows/settings-advanced.md": "90d913043cd11502",
|
||||
"gsd-core/workflows/settings-integrations.md": "f8f756709ec02363",
|
||||
"gsd-core/workflows/settings.md": "44b10c59215633b8",
|
||||
"gsd-core/workflows/ship.md": "c08f0fe3025d2c86",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/scan.md": "12c11b2edc165df9",
|
||||
"gsd-core/workflows/secure-phase.md": "7bf923689bf58288",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "76399a40da18f0b4",
|
||||
"gsd-core/workflows/settings-advanced.md": "18eff9c17e0b1948",
|
||||
"gsd-core/workflows/settings-integrations.md": "b082fc518b484c07",
|
||||
"gsd-core/workflows/settings.md": "002eb0ce3c10c741",
|
||||
"gsd-core/workflows/ship.md": "c035bb8b3bb7efbb",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "eee64a2cf3b94c00",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "77ada5fe336ed9f7",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
|
||||
@@ -74,9 +74,15 @@ describe('extensionEventSurfaceFor (extension-system event dialect — #1943)',
|
||||
assert.equal(extensionEventSurfaceFor('nope'), null);
|
||||
assert.equal(extensionEventSurfaceFor(undefined), null);
|
||||
});
|
||||
test('EXTENSION_EVENT_SURFACES is frozen + covers opencode/pi/hermes/none', () => {
|
||||
test('EXTENSION_EVENT_SURFACES is frozen + covers opencode/pi/hermes/kilo/none', () => {
|
||||
assert.equal(Object.isFrozen(EXTENSION_EVENT_SURFACES), true);
|
||||
assert.deepEqual(Object.keys(EXTENSION_EVENT_SURFACES).sort(), ['hermes', 'none', 'opencode', 'pi']);
|
||||
assert.deepEqual(Object.keys(EXTENSION_EVENT_SURFACES).sort(), ['hermes', 'kilo', 'none', 'opencode', 'pi']);
|
||||
});
|
||||
test('kilo reuses the IDENTICAL event array as opencode (Kilo is an OpenCode fork, same bus — #2093)', () => {
|
||||
const kiloSurface = extensionEventSurfaceFor('kilo');
|
||||
const opencodeSurface = extensionEventSurfaceFor('opencode');
|
||||
assert.ok(kiloSurface, 'kilo must resolve (non-null) — it is a consumed extensionEvents value');
|
||||
assert.deepEqual(kiloSurface, opencodeSurface);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -702,6 +702,36 @@ describe('issue #2612: runtime "opencode" — OpenCode tier resolution', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issue #2093: kilo runtime tier resolution ───────────────────────────────
|
||||
// Kilo is an OpenCode fork and shares the IDENTICAL built-in tier IDs (UPGRADE 2
|
||||
// / ADR-1239). Kilo moved from Group B (no built-in defaults) to Group A here.
|
||||
describe('issue #2093: runtime "kilo" — Kilo tier resolution', () => {
|
||||
let tmpDir;
|
||||
beforeEach(() => { isolateHome(); tmpDir = createTempProject(); resetRuntimeWarningCaches(); });
|
||||
afterEach(() => { cleanup(tmpDir); restoreHome(); });
|
||||
|
||||
test('opus tier -> anthropic/claude-opus-4-8', () => {
|
||||
writeConfig(tmpDir, { runtime: 'kilo', model_profile: 'quality' });
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'anthropic/claude-opus-4-8');
|
||||
});
|
||||
|
||||
test('sonnet tier -> anthropic/claude-sonnet-5', () => {
|
||||
writeConfig(tmpDir, { runtime: 'kilo', model_profile: 'balanced' });
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-5');
|
||||
});
|
||||
|
||||
test('haiku tier -> anthropic/claude-haiku-4-5', () => {
|
||||
writeConfig(tmpDir, { runtime: 'kilo', model_profile: 'budget' });
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'anthropic/claude-haiku-4-5');
|
||||
});
|
||||
|
||||
test('kilo: effort resolves universally but render param is null (no wire param)', () => {
|
||||
writeConfig(tmpDir, { runtime: 'kilo', model_profile: 'quality' });
|
||||
const eff = resolveEffortInternal(tmpDir, 'gsd-planner');
|
||||
assert.strictEqual(renderEffortForRuntime('kilo', eff).param, null);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issue #2612: copilot runtime tier resolution ────────────────────────────
|
||||
describe('issue #2612: runtime "copilot" — Copilot tier resolution', () => {
|
||||
let tmpDir;
|
||||
@@ -736,10 +766,6 @@ describe('issue #2612: Group B runtimes — no built-in map, use unknown-runtime
|
||||
assert.strictEqual(RUNTIME_PROFILE_MAP.cursor, undefined);
|
||||
});
|
||||
|
||||
test('kilo is not in RUNTIME_PROFILE_MAP', () => {
|
||||
assert.strictEqual(RUNTIME_PROFILE_MAP.kilo, undefined);
|
||||
});
|
||||
|
||||
test('windsurf is not in RUNTIME_PROFILE_MAP', () => {
|
||||
assert.strictEqual(RUNTIME_PROFILE_MAP.windsurf, undefined);
|
||||
});
|
||||
@@ -830,4 +856,21 @@ describe('issue #2612: partial override merge for new Group A runtimes', () => {
|
||||
// gsd-planner budget -> sonnet -> built-in default
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-sonnet-5');
|
||||
});
|
||||
|
||||
// #2093: kilo just moved into Group A — same partial-merge coverage as opencode.
|
||||
test('kilo.sonnet override wins; opus and haiku still use built-in defaults', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'kilo',
|
||||
model_profile: 'balanced',
|
||||
model_profile_overrides: {
|
||||
kilo: { sonnet: 'anthropic/claude-sonnet-4-7' },
|
||||
},
|
||||
});
|
||||
// gsd-planner balanced -> opus -> built-in default
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'anthropic/claude-opus-4-8');
|
||||
// gsd-roadmapper balanced -> sonnet -> overridden
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-4-7');
|
||||
// gsd-codebase-mapper balanced -> haiku -> built-in default (haiku not overridden)
|
||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'anthropic/claude-haiku-4-5');
|
||||
});
|
||||
});
|
||||
|
||||
182
tests/kilo-imperative-reference.test.cjs
Normal file
182
tests/kilo-imperative-reference.test.cjs
Normal file
@@ -0,0 +1,182 @@
|
||||
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'kilo'` string-equality branch (nor an `isKilo` logic branch) remains in bin/install.js, src/install-engine.cts, src/runtime-artifact-conversion.cts, and src/runtime-artifact-layout.cts — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2093)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* kilo imperative reference host — ADR-1239 Phase D / #2093 (EoS/kilo).
|
||||
*
|
||||
* Proves Kilo Code is driven through the PUBLIC Host-Integration Interface
|
||||
* (the imperative adapter), that its negotiated axes classify + negotiate
|
||||
* correctly, that negotiation fails CLOSED on a corrupted descriptor, and
|
||||
* that the migration retired the hardcoded `runtime === 'kilo'` / `isKilo`
|
||||
* branches across the install engine, artifact conversion, and artifact
|
||||
* layout modules (folded into descriptor-driven `runtime.hostBehaviors`).
|
||||
*
|
||||
* Kilo is an OpenCode fork (same plugin/extension event bus, same static
|
||||
* agent-frontmatter model constraint) but its `dispatch.subagentToolkit` is
|
||||
* `'undocumented'` — no authoritative Kilo doc states a default subagent
|
||||
* toolkit level — so unlike OpenCode/Qwen/Cursor, Kilo's dispatch interface
|
||||
* point degrades to `'degraded'`, not `'full'`, even though every other
|
||||
* dispatch axis (namedDispatch/nested/maxDepth/background) matches the
|
||||
* unbounded-depth programmatic-cli baseline. This is the fail-closed
|
||||
* negotiation contract working as designed (see AC-specific test below).
|
||||
* The real upgrades (native `.kilo/plugins/gsd-core.js` hook-bus plugin,
|
||||
* active-model routing, MCP companion doc, named agent dispatch) are covered
|
||||
* in tests/kilo-upgrades.test.cjs.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
degradationFor,
|
||||
extensionEventSurfaceFor,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const KILO_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'kilo', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const KILO_AXES = KILO_CAP.runtime.hostIntegration;
|
||||
|
||||
// -- AC2: driven through the public interface (imperative adapter) -----------
|
||||
|
||||
test('createImperativeAdapter classifies kilo as imperative + composes the registry', () => {
|
||||
const adapter = createImperativeAdapter({ runtime: 'kilo' });
|
||||
assert.equal(adapter.kind, 'imperative');
|
||||
assert.equal(adapter.runtime, 'kilo');
|
||||
assert.ok(adapter.registry && typeof adapter.registry === 'object');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('kilo axes classify as the programmatic-cli reference profile', () => {
|
||||
// Confirmed via `node -e` against the real descriptor before asserting:
|
||||
// profileOf(KILO_AXES) === 'programmatic-cli' (embeddingMode: 'imperative').
|
||||
assert.equal(profileOf(KILO_AXES), 'programmatic-cli');
|
||||
});
|
||||
|
||||
// -- AC3: all axes populated + validated -------------------------------------
|
||||
|
||||
test('kilo descriptor declares all 8 axes + 6 dispatch sub-axes with exact values', () => {
|
||||
assert.equal(KILO_AXES.embeddingMode, 'imperative');
|
||||
assert.equal(KILO_AXES.commandSurface, 'slash-file');
|
||||
assert.equal(KILO_AXES.modelMode, 'active');
|
||||
assert.equal(KILO_AXES.hookBus, 'host');
|
||||
assert.equal(KILO_AXES.stateIO, 'filesystem');
|
||||
assert.equal(KILO_AXES.transport, 'mcp');
|
||||
assert.equal(KILO_AXES.runtime, 'bun');
|
||||
const d = KILO_AXES.dispatch;
|
||||
assert.equal(d.namedDispatch, true);
|
||||
assert.equal(d.nested, true);
|
||||
assert.equal(d.maxDepth, -1);
|
||||
assert.equal(d.background, true);
|
||||
assert.equal(d.subagentToolkit, 'undocumented');
|
||||
assert.equal(d.backgroundDispatch, false);
|
||||
});
|
||||
|
||||
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for kilo, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KILO_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...KILO_AXES, embeddingMode: 'future-unknown' }));
|
||||
});
|
||||
|
||||
test('AC-SPECIFIC: kilo real axes degrade dispatch to "degraded", not "full", because subagentToolkit is "undocumented"', () => {
|
||||
// Confirmed via `node -e` against the real descriptor before asserting:
|
||||
// degradationFor('dispatch', { dispatch: KILO_AXES.dispatch }) returns
|
||||
// { level: 'degraded', fallback: 'restricted/undocumented subagent toolkit — limited dispatch surface' }.
|
||||
//
|
||||
// Every OTHER dispatch axis matches the unbounded-depth programmatic-cli
|
||||
// baseline (namedDispatch:true, nested:true, maxDepth:-1 = unbounded,
|
||||
// background:true) — the degradationFor 'dispatch' interface point would
|
||||
// return 'full' for that shape IF subagentToolkit were 'full' (see
|
||||
// src/host-integration.cts degradationFor, the `disp.subagentToolkit === 'full'`
|
||||
// gate ~line 183). Kilo's subagentToolkit is 'undocumented' (no authoritative
|
||||
// default toolkit level is documented for Kilo subagents), so the gate fails
|
||||
// closed and dispatch degrades instead of reporting full capability.
|
||||
const result = degradationFor('dispatch', { dispatch: KILO_AXES.dispatch });
|
||||
assert.equal(result.level, 'degraded');
|
||||
assert.notEqual(result.level, 'full');
|
||||
assert.match(result.fallback, /undocumented subagent toolkit/);
|
||||
|
||||
// Sanity: if subagentToolkit WERE 'full' (all else equal), the same shape
|
||||
// would degrade to 'full' — proving the gate is what flips the result, not
|
||||
// some other axis.
|
||||
const hypotheticalFull = degradationFor('dispatch', { dispatch: { ...KILO_AXES.dispatch, subagentToolkit: 'full' } });
|
||||
assert.equal(hypotheticalFull.level, 'full');
|
||||
});
|
||||
|
||||
test('a partial/empty kilo descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['programmatic-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
// -- AC2: the folded-in behaviors ---------------------------------------------
|
||||
|
||||
test('kilo descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
|
||||
const hb = KILO_CAP.runtime.hostBehaviors;
|
||||
assert.ok(hb && typeof hb === 'object');
|
||||
assert.equal(hb.attributionConfigResolver, 'kilo');
|
||||
assert.equal(hb.flatCommandDir, 'command');
|
||||
assert.equal(hb.combinedFamilyInstall, true);
|
||||
assert.equal(hb.frontmatterDialect, 'kilo');
|
||||
assert.equal(hb.skipUpdateBannerCommand, true);
|
||||
assert.equal(hb.skipSharedHooksInstall, true);
|
||||
assert.ok(hb.nativePlugin && typeof hb.nativePlugin === 'object');
|
||||
assert.equal(hb.nativePlugin.dir, 'plugins');
|
||||
assert.equal(hb.nativePlugin.file, 'gsd-core.js');
|
||||
assert.equal(hb.nativePlugin.source, '.kilo/plugins/gsd-core.js');
|
||||
});
|
||||
|
||||
// -- UPGRADE 1 dialect: kilo shares OpenCode's extension-event bus -----------
|
||||
|
||||
test('kilo declares extensionEvents:"kilo" and its event surface equals OpenCode\'s (Kilo is an OpenCode fork)', () => {
|
||||
assert.equal(KILO_CAP.runtime.extensionEvents, 'kilo');
|
||||
const kiloSurface = extensionEventSurfaceFor('kilo');
|
||||
const opencodeSurface = extensionEventSurfaceFor('opencode');
|
||||
assert.ok(Array.isArray(kiloSurface) && kiloSurface.length > 0, 'kilo is a consumed extensionEvents dialect (non-empty surface)');
|
||||
assert.deepEqual(kiloSurface, opencodeSurface, 'kilo reuses OPENCODE_EXTENSION_EVENTS verbatim — same bus');
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branches are retired across all folded modules -------
|
||||
|
||||
test('no `runtime === "kilo"` / `isKilo` logic branch remains in the descriptor-migrated modules (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const files = [
|
||||
path.join(repoRoot, 'bin', 'install.js'),
|
||||
path.join(repoRoot, 'src', 'install-engine.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-layout.cts'),
|
||||
];
|
||||
for (const file of files) {
|
||||
const stripped = strip(fs.readFileSync(file, 'utf8'));
|
||||
|
||||
const equalityOffenders = stripped.match(/runtime\s*[!=]==\s*'kilo'/g) || [];
|
||||
assert.deepEqual(equalityOffenders, [],
|
||||
`AC2: no hardcoded runtime==='kilo' branch may remain in ${path.relative(repoRoot, file)}; found: ${equalityOffenders.join(', ')}`);
|
||||
|
||||
// A plain `isKilo` binding (e.g. destructured off runtimeFlags()) is fine —
|
||||
// it's the LOGIC branches keyed on it that must be retired.
|
||||
const logicOffenders = [
|
||||
...(stripped.match(/if\s*\(\s*isKilo/g) || []),
|
||||
...(stripped.match(/isKilo\s*[&|?]/g) || []),
|
||||
...(stripped.match(/[&|]\s*isKilo/g) || []),
|
||||
];
|
||||
assert.deepEqual(logicOffenders, [],
|
||||
`AC2: no isKilo logic branch may remain in ${path.relative(repoRoot, file)}; found: ${logicOffenders.join(', ')}`);
|
||||
}
|
||||
});
|
||||
295
tests/kilo-upgrades.test.cjs
Normal file
295
tests/kilo-upgrades.test.cjs
Normal file
@@ -0,0 +1,295 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* kilo capability UPGRADES — ADR-1239 Phase D / #2093 (EoS/kilo).
|
||||
*
|
||||
* Drives the user-reachable surface (spawned `bin/install.js` via
|
||||
* `runMinimalInstall`) plus targeted unit coverage to prove the four real
|
||||
* upgrades Kilo contributes as part of the imperative-adapter migration:
|
||||
*
|
||||
* UPGRADE 1 — native hook-bus plugin: `.kilo/plugins/gsd-core.js`, a
|
||||
* byte-identical copy of `.opencode/plugins/gsd-core.js` (Kilo is an
|
||||
* OpenCode fork sharing the same plugin/extension event bus).
|
||||
*
|
||||
* UPGRADE 2 — active-model routing: `convertClaudeToKiloFrontmatter` now
|
||||
* emits a `model:` field from the resolved model override instead of
|
||||
* always stripping it (mirrors the OpenCode upgrade, #2256).
|
||||
*
|
||||
* UPGRADE 3 — MCP companion documented + reachable: `docs/how-to/connect-gsd-mcp-server.md`
|
||||
* covers Kilo's `mcp`-keyed config (not `mcpServers`), and the companion the
|
||||
* doc points at (`bin/gsd-mcp-server.js`) is proven live by spawning it and
|
||||
* performing a real initialize + tools/list handshake (AC4: "test: connect
|
||||
* and list tools") — mirrors tests/gsd-mcp-server-bin.test.cjs exactly.
|
||||
*
|
||||
* UPGRADE 4 — named subagent dispatch: GSD's specialist agents install as
|
||||
* `<configDir>/agents/gsd-*.md` with `mode: subagent` + a `permission:`
|
||||
* block — the slug Kilo's Task tool dispatches by.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { listAgentFiles } = require('./helpers/agent-roster.cjs');
|
||||
const { convertClaudeToKiloFrontmatter } = require('../bin/install.js');
|
||||
const { PROTOCOL_VERSION } = require('../gsd-core/bin/lib/mcp-server.cjs');
|
||||
|
||||
const MCP_SERVER_BIN = path.join(__dirname, '..', 'bin', 'gsd-mcp-server.js');
|
||||
|
||||
const KILO_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'kilo', 'capability.json'), 'utf8'),
|
||||
);
|
||||
|
||||
const ADAPTER_SRC = path.join(__dirname, '..', '.kilo', 'plugins', 'gsd-core.js');
|
||||
const OPENCODE_ADAPTER_SRC = path.join(__dirname, '..', '.opencode', 'plugins', 'gsd-core.js');
|
||||
|
||||
/** Extract the YAML frontmatter block (between the first pair of `---` lines), or null. */
|
||||
function parseFrontmatter(content) {
|
||||
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 1: native hook-bus plugin (.kilo/plugins/gsd-core.js)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const scope of ['global', 'local']) {
|
||||
test(`kilo --${scope}: installs .../plugins/gsd-core.js byte-identical to the repo source (UPGRADE 1)`, (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'kilo', scope });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const installedPluginPath = path.join(configDir, 'plugins', 'gsd-core.js');
|
||||
assert.ok(fs.existsSync(installedPluginPath), `${installedPluginPath} must exist`);
|
||||
|
||||
const installed = fs.readFileSync(installedPluginPath);
|
||||
const source = fs.readFileSync(ADAPTER_SRC);
|
||||
assert.ok(installed.equals(source), 'installed plugin must byte-equal the repo .kilo/plugins/gsd-core.js source');
|
||||
});
|
||||
}
|
||||
|
||||
// Faithful emulation of a plugin loader: `getServerPlugin` accepts a bare
|
||||
// function OR an object with a `.server` function (mirrors the OpenCode
|
||||
// loader contract Kilo forked, tests/opencode-plugin-adapter.test.cjs).
|
||||
function getServerPlugin(entry) {
|
||||
if (typeof entry === 'function') return entry;
|
||||
if (entry && typeof entry === 'object' && typeof entry.server === 'function') return entry.server;
|
||||
return null;
|
||||
}
|
||||
function loaderExtract(mod) {
|
||||
const servers = [];
|
||||
for (const entry of Object.values(mod)) {
|
||||
const s = getServerPlugin(entry);
|
||||
if (!s) throw new TypeError('Plugin export is not a function');
|
||||
servers.push(s);
|
||||
}
|
||||
return servers;
|
||||
}
|
||||
|
||||
test('.kilo/plugins/gsd-core.js loads as raw CommonJS and exposes id "gsd-core" + server._internals (UPGRADE 1)', () => {
|
||||
const mod = require(ADAPTER_SRC);
|
||||
assert.equal(mod.id, 'gsd-core');
|
||||
// NON-ENUMERABLE so it never lands in Object.values (would throw in the loader loop).
|
||||
assert.ok(!Object.keys(mod).includes('id'), 'id must be non-enumerable');
|
||||
const servers = loaderExtract(mod); // must not throw
|
||||
assert.equal(servers.length, 1);
|
||||
assert.equal(typeof servers[0], 'function');
|
||||
assert.equal(typeof mod.server._internals, 'object');
|
||||
assert.ok(mod.server._internals, 'server._internals must be present');
|
||||
});
|
||||
|
||||
// DEFECT.GENERATIVE-FIX parity guard: .kilo/plugins/gsd-core.js is a deliberate
|
||||
// byte-copy of .opencode/plugins/gsd-core.js (Kilo is an OpenCode fork sharing
|
||||
// the same plugin/extension event bus, see the UPGRADE 1 doc comment above).
|
||||
// Nothing enforces that copy relationship — a future edit to either file that
|
||||
// forgets its twin would silently drift the two runtimes apart. This fails the
|
||||
// instant that happens.
|
||||
test('.kilo/plugins/gsd-core.js stays byte-identical to .opencode/plugins/gsd-core.js (Kilo is an OpenCode fork; parity guard, UPGRADE 1)', () => {
|
||||
const kilo = fs.readFileSync(ADAPTER_SRC, 'utf8');
|
||||
const opencode = fs.readFileSync(OPENCODE_ADAPTER_SRC, 'utf8');
|
||||
assert.equal(
|
||||
kilo,
|
||||
opencode,
|
||||
'.kilo/plugins/gsd-core.js and .opencode/plugins/gsd-core.js must stay byte-identical — ' +
|
||||
'Kilo is an OpenCode fork and intentionally reuses the same plugin verbatim; if you edited ' +
|
||||
'one, mirror the change into the other (or this guard will keep failing).',
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 2: active-model routing (convertClaudeToKiloFrontmatter)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SAMPLE_AGENT = `---
|
||||
name: gsd-executor
|
||||
description: Executes GSD plans with atomic commits
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
color: yellow
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a GSD plan executor.
|
||||
</role>`;
|
||||
|
||||
const SAMPLE_COMMAND = `---
|
||||
name: gsd-execute-phase
|
||||
description: Execute all plans in a phase
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
---
|
||||
|
||||
Execute the phase plan.`;
|
||||
|
||||
test('UPGRADE 2: convertClaudeToKiloFrontmatter emits model: when isAgent + modelOverride is provided', () => {
|
||||
const result = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.match(frontmatter, /^model: anthropic\/claude-sonnet-5$/m, 'model: field must carry the resolved override');
|
||||
});
|
||||
|
||||
test('UPGRADE 2: convertClaudeToKiloFrontmatter emits NO model: when isAgent + modelOverride is null', () => {
|
||||
const result = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!/^model:/m.test(frontmatter), 'model: field must be absent when no override is resolved');
|
||||
});
|
||||
|
||||
test('UPGRADE 2: convertClaudeToKiloFrontmatter emits NO model: for commands, even with a modelOverride (commands strip)', () => {
|
||||
const result = convertClaudeToKiloFrontmatter(SAMPLE_COMMAND, { isAgent: false, modelOverride: 'x' });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!/^model:/m.test(frontmatter), 'commands never carry a model: field, regardless of modelOverride');
|
||||
});
|
||||
|
||||
// Note: a bare runMinimalInstall does NOT configure a runtime model_overrides/
|
||||
// model_profile_overrides config, so installed agents will NOT carry a model:
|
||||
// line from a plain install — that is expected (readGsdEffectiveModelOverrides
|
||||
// / readGsdRuntimeProfileResolver resolve to nothing) and is NOT a regression.
|
||||
// The unit tests above are the correct surface for proving U2's "stop
|
||||
// stripping, emit requested model" behavior change.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 3: MCP companion documented
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// allow-test-rule: docs-parity (#2093) — docs/how-to/connect-gsd-mcp-server.md
|
||||
// must document Kilo's real mcp-keyed config (not mcpServers); the doc prose IS
|
||||
// the canonical statement of that fact and there is no runtime API to enumerate
|
||||
// it, so reading the file and asserting on its text is the only parity check
|
||||
// available.
|
||||
test('UPGRADE 3: docs/how-to/connect-gsd-mcp-server.md documents Kilo\'s mcp-keyed config', () => {
|
||||
const docPath = path.join(__dirname, '..', 'docs', 'how-to', 'connect-gsd-mcp-server.md');
|
||||
const doc = fs.readFileSync(docPath, 'utf8');
|
||||
assert.match(doc, /Kilo/, 'doc must mention Kilo');
|
||||
assert.match(doc, /`mcp` key \(\*\*not\*\* `mcpServers`\)/,
|
||||
'doc must call out the mcp (not mcpServers) key for Kilo/OpenCode');
|
||||
assert.ok(doc.includes('"mcp"'), 'doc must show the literal "mcp" config key');
|
||||
assert.ok(doc.includes('opencode.jsonc') || doc.includes('opencode.json'),
|
||||
'doc must name Kilo\'s native config file (shared with OpenCode\'s schema)');
|
||||
});
|
||||
|
||||
// AC4 ("test: connect and list tools"): prove the companion Kilo's `mcp` config
|
||||
// points at (bin/gsd-mcp-server.js) is actually reachable, not just documented.
|
||||
// Mirrors tests/gsd-mcp-server-bin.test.cjs's spawn/handshake mechanism exactly
|
||||
// (same shim, same line-delimited JSON-RPC over stdio, same clean-exit-on-EOF
|
||||
// contract) rather than reinventing the protocol handshake.
|
||||
test('UPGRADE 3: gsd-mcp-server companion is reachable — spawn, initialize, tools/list over stdio (AC4)', () => {
|
||||
const stdin = [
|
||||
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }),
|
||||
JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }),
|
||||
].join('\n') + '\n';
|
||||
|
||||
const res = spawnSync(process.execPath, [MCP_SERVER_BIN], {
|
||||
input: stdin,
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
env: { ...process.env, GSD_TEST_MODE: '1' },
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 0, `gsd-mcp-server must exit cleanly on stdin EOF; stderr: ${res.stderr}`);
|
||||
const lines = res.stdout.trim().split('\n').map((l) => JSON.parse(l));
|
||||
assert.strictEqual(lines.length, 2, 'one response per request');
|
||||
assert.strictEqual(lines[0].id, 1);
|
||||
assert.strictEqual(lines[0].result.protocolVersion, PROTOCOL_VERSION, 'initialize handshake succeeds');
|
||||
|
||||
const toolNames = lines[1].result.tools.map((t) => t.name).sort();
|
||||
assert.deepStrictEqual(
|
||||
toolNames,
|
||||
['gsd_invoke_command', 'gsd_read_state', 'gsd_write_state'],
|
||||
'the companion Kilo\'s mcp config connects to advertises the real GSD tool surface',
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 4: named subagent dispatch (agents/*.md, mode: subagent)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const KILO_AGENT_PERMISSION_KEYS = [
|
||||
'read', 'edit', 'bash', 'grep', 'glob', 'task',
|
||||
'webfetch', 'websearch', 'skill', 'question', 'todowrite', 'list', 'codesearch', 'lsp',
|
||||
];
|
||||
|
||||
for (const scope of ['global', 'local']) {
|
||||
test(`kilo --${scope}: native agents/*.md subagent projection with mode: subagent (UPGRADE 4)`, (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'kilo', scope });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
assert.ok(fs.existsSync(agentsDir), `${agentsDir} must exist`);
|
||||
|
||||
const expectedNames = listAgentFiles();
|
||||
assert.equal(expectedNames.length, 34,
|
||||
'sanity: shipped GSD agent roster is 34 files — update this boundary if the roster changes');
|
||||
|
||||
const installedFiles = fs.readdirSync(agentsDir)
|
||||
.filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(installedFiles.length >= expectedNames.length,
|
||||
`expected at least ${expectedNames.length} installed agents under ${agentsDir}, got ${installedFiles.length}`);
|
||||
for (const name of expectedNames) {
|
||||
assert.ok(installedFiles.includes(`${name}.md`), `${name}.md must be installed under ${agentsDir}`);
|
||||
}
|
||||
|
||||
for (const known of ['gsd-code-reviewer', 'gsd-planner', 'gsd-executor']) {
|
||||
const filePath = path.join(agentsDir, `${known}.md`);
|
||||
assert.ok(fs.existsSync(filePath), `${filePath} must exist`);
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const fm = parseFrontmatter(content);
|
||||
assert.ok(fm, `${known}.md must have YAML frontmatter`);
|
||||
|
||||
assert.match(fm, /^name:\s*\S+/m, `${known}.md frontmatter must declare name:`);
|
||||
assert.match(fm, /^mode:\s*subagent\s*$/m,
|
||||
`${known}.md frontmatter must declare mode: subagent (the slug Kilo's Task tool dispatches by)`);
|
||||
|
||||
assert.match(fm, /^permission:\s*$/m, `${known}.md frontmatter must declare a permission: block`);
|
||||
for (const key of KILO_AGENT_PERMISSION_KEYS) {
|
||||
assert.match(fm, new RegExp(`^\\s+${key}:\\s*(allow|deny)\\s*$`, 'm'),
|
||||
`${known}.md permission: block must declare ${key}: allow|deny`);
|
||||
}
|
||||
|
||||
// Branding-residue checks are scoped to the FRONTMATTER — the part the
|
||||
// opencode/kilo converter fully rewrites into Kilo-native form. The agent
|
||||
// BODY legitimately retains Claude-Code source references byte-identical to
|
||||
// opencode's installed agents (verified): the shared runtime-launcher shell
|
||||
// preamble's git-root `.claude/` fallback
|
||||
// (`${RUNTIME_DIR:-$(git rev-parse --show-toplevel)/.claude/…}`) and prose
|
||||
// product-name mentions (e.g. "…inside a Claude Code worktree…"). These are
|
||||
// family-wide launcher/prose artifacts, not kilo conversion defects — the
|
||||
// opencode/kilo family, unlike qwen's aggressive converter, does not rewrite
|
||||
// body prose.
|
||||
assert.ok(!fm.includes('CLAUDE.md'), `${known}.md frontmatter must not contain residual "CLAUDE.md"`);
|
||||
assert.ok(!fm.includes('Claude Code'), `${known}.md frontmatter must not contain residual "Claude Code"`);
|
||||
assert.ok(!fm.includes('.claude/'), `${known}.md frontmatter must not contain residual ".claude/"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- boundary/negative: hooksSurface:'none' + subagentToolkit stays undocumented
|
||||
|
||||
test('capabilities/kilo/capability.json extendedHookEvents is exactly [] (hooksSurface: "none") and dispatch.subagentToolkit stays "undocumented"', () => {
|
||||
assert.deepEqual(KILO_CAP.runtime.extendedHookEvents, []);
|
||||
assert.equal(KILO_CAP.runtime.hooksSurface, 'none');
|
||||
assert.equal(KILO_CAP.runtime.hostIntegration.dispatch.subagentToolkit, 'undocumented');
|
||||
});
|
||||
@@ -229,6 +229,44 @@ Fallback skills live in .agents/skills/.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeToKiloFrontmatter is
|
||||
// defined TWICE — once in bin/install.js (the function bound at the top of this
|
||||
// file, used by bin/install.js's own legacy install path) and once in
|
||||
// src/runtime-artifact-conversion.cts, compiled to
|
||||
// gsd-core/bin/lib/runtime-artifact-conversion.cjs (used by
|
||||
// src/install-engine.cts's newer TS install path, see install-engine.cts ~L871).
|
||||
// Both copies are LIVE — neither re-exports the other — so #2093's modelOverride
|
||||
// edit had to be applied twice by hand. Source-text identity can't be asserted
|
||||
// (they live in different module systems: plain CJS vs a tsc-compiled .cts
|
||||
// output with different surrounding comments), so this instead proves the two
|
||||
// implementations still produce IDENTICAL output for representative agent and
|
||||
// command input. If a future edit changes one copy's behavior without mirroring
|
||||
// it into the other, this test is the guard that catches the divergence.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe('convertClaudeToKiloFrontmatter output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2093)', () => {
|
||||
const { convertClaudeToKiloFrontmatter: convertViaConversionModule } =
|
||||
require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
||||
|
||||
test('identical output for an agent with a model override', () => {
|
||||
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
|
||||
const viaModule = convertViaConversionModule(SAMPLE_AGENT, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
|
||||
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical agent output');
|
||||
});
|
||||
|
||||
test('identical output for an agent with no model override', () => {
|
||||
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
|
||||
const viaModule = convertViaConversionModule(SAMPLE_AGENT, { isAgent: true, modelOverride: null });
|
||||
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical agent output with no override');
|
||||
});
|
||||
|
||||
test('identical output for a command (model override never applies)', () => {
|
||||
const viaInstall = convertClaudeToKiloFrontmatter(SAMPLE_COMMAND, { isAgent: false, modelOverride: 'x' });
|
||||
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, { isAgent: false, modelOverride: 'x' });
|
||||
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical command output');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Antigravity agent conversion — shared Gemini-backend tool mapping (#1394 / #1928)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Stale-bake guard tests (#1688, follow-up to #1650).
|
||||
*
|
||||
* Regression contract: on a static-frontmatter runtime (codex/opencode), if
|
||||
* Regression contract: on a static-frontmatter runtime (codex/opencode/kilo), if
|
||||
* `.planning/config.json` or `~/.gsd/defaults.json` was edited AFTER the
|
||||
* installed agent files were baked, `warnIfStaleBake` MUST emit a single
|
||||
* stderr warning naming the config path and the remediation command. Before
|
||||
@@ -71,6 +71,13 @@ describe('stale-bake-guard.detectStaleBake (pure decision)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('kilo runtime honored symmetrically with opencode/codex (#2093)', () => {
|
||||
assert.deepEqual(
|
||||
detectStaleBake({ runtime: 'kilo', configMtimeMs: AGENT_MS + 5000, agentMtimeMs: AGENT_MS }),
|
||||
{ stale: true, deltaMs: 5000 },
|
||||
);
|
||||
});
|
||||
|
||||
test('non-finite mtimes rejected (NaN / Infinity)', () => {
|
||||
assert.equal(detectStaleBake({ runtime: 'opencode', configMtimeMs: NaN, agentMtimeMs: AGENT_MS }), null);
|
||||
assert.equal(detectStaleBake({ runtime: 'opencode', configMtimeMs: Infinity, agentMtimeMs: AGENT_MS }), null);
|
||||
@@ -114,6 +121,13 @@ describe('stale-bake-guard.formatStaleBakeWarning (pure formatter)', () => {
|
||||
assert.match(w, /gsd install --codex/);
|
||||
assert.doesNotMatch(w, /--opencode/);
|
||||
});
|
||||
|
||||
test('kilo warning uses --kilo flag (not --opencode/--codex) (#2093)', () => {
|
||||
const w = formatStaleBakeWarning({ runtime: 'kilo', configPath: '/x', configMtimeMs: 1_700_000_000_000, agentMtimeMs: 1 });
|
||||
assert.match(w, /gsd install --kilo/);
|
||||
assert.doesNotMatch(w, /--opencode/);
|
||||
assert.doesNotMatch(w, /--codex\b/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -143,11 +157,17 @@ describe('stale-bake-guard.resolveAgentDir (env-var aware)', () => {
|
||||
// the raw return — that fails windows-latest CI.
|
||||
const posix = (p) => String(p).replace(/\\/g, '/');
|
||||
|
||||
test('opencode default lands under ~/.config/opencode/agent', () => {
|
||||
assert.equal(posix(resolveAgentDir('opencode', { env: {}, homedir: () => '/H' })), '/H/.config/opencode/agent');
|
||||
// #2093: fixed from the stale singular `.../agent` — the real installer
|
||||
// writes to the plural `agents/` dir (bin/install.js's universal
|
||||
// `agentsDest = path.join(targetDir, 'agents')`), verified against a live
|
||||
// `--opencode --global` install. The prior singular assertion here matched
|
||||
// the (wrong) implementation, which made `warnIfStaleBake` a silent no-op
|
||||
// for opencode in production — see resolveAgentDir's inline comment.
|
||||
test('opencode default lands under ~/.config/opencode/agents', () => {
|
||||
assert.equal(posix(resolveAgentDir('opencode', { env: {}, homedir: () => '/H' })), '/H/.config/opencode/agents');
|
||||
});
|
||||
test('opencode honors OPENCODE_CONFIG_DIR', () => {
|
||||
assert.equal(posix(resolveAgentDir('opencode', { env: { OPENCODE_CONFIG_DIR: '/custom/oc' }, homedir: () => '/H' })), '/custom/oc/agent');
|
||||
assert.equal(posix(resolveAgentDir('opencode', { env: { OPENCODE_CONFIG_DIR: '/custom/oc' }, homedir: () => '/H' })), '/custom/oc/agents');
|
||||
});
|
||||
test('codex default lands under ~/.codex/agents', () => {
|
||||
assert.equal(posix(resolveAgentDir('codex', { env: {}, homedir: () => '/H' })), '/H/.codex/agents');
|
||||
@@ -155,6 +175,12 @@ describe('stale-bake-guard.resolveAgentDir (env-var aware)', () => {
|
||||
test('codex honors CODEX_HOME', () => {
|
||||
assert.equal(posix(resolveAgentDir('codex', { env: { CODEX_HOME: '/custom/cx' }, homedir: () => '/H' })), '/custom/cx/agents');
|
||||
});
|
||||
test('kilo default lands under ~/.config/kilo/agents (#2093)', () => {
|
||||
assert.equal(posix(resolveAgentDir('kilo', { env: {}, homedir: () => '/H' })), '/H/.config/kilo/agents');
|
||||
});
|
||||
test('kilo honors KILO_CONFIG_DIR (#2093)', () => {
|
||||
assert.equal(posix(resolveAgentDir('kilo', { env: { KILO_CONFIG_DIR: '/custom/ko' }, homedir: () => '/H' })), '/custom/ko/agents');
|
||||
});
|
||||
test('unsupported runtime → null', () => {
|
||||
assert.equal(resolveAgentDir('gemini', { env: {}, homedir: () => '/H' }), null);
|
||||
});
|
||||
@@ -210,11 +236,14 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
function cxEnv(agentParentDir) {
|
||||
return { CODEX_HOME: agentParentDir };
|
||||
}
|
||||
function koEnv(agentParentDir) {
|
||||
return { KILO_CONFIG_DIR: agentParentDir };
|
||||
}
|
||||
|
||||
test('claude runtime → no warning even when config is newer than agents', () => {
|
||||
// OpenCode agent dir shape so the runtime path resolves, but runtime is claude.
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'claude', configMtime: NEWER, agentDir, agentMtime: OLDER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) });
|
||||
assert.equal(wrote, false);
|
||||
@@ -223,7 +252,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('opencode: config NEWER than agents → warning written (the #1650 failure mode)', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: NEWER, agentDir, agentMtime: OLDER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) });
|
||||
assert.equal(wrote, true);
|
||||
@@ -242,9 +271,31 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
assert.match(chunks[0], /gsd install --codex/);
|
||||
});
|
||||
|
||||
// #2093: kilo bakes model: like opencode (same static-frontmatter constraint).
|
||||
test('kilo: config NEWER than agents → warning written with --kilo flag', () => {
|
||||
const agentParent = path.join(tmpRoot, 'ko-config');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'kilo', configMtime: NEWER, agentDir, agentMtime: OLDER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: koEnv(agentParent) });
|
||||
assert.equal(wrote, true);
|
||||
assert.equal(chunks.length, 1);
|
||||
assert.match(chunks[0], /model config in .*config\.json changed since agents were last baked/);
|
||||
assert.match(chunks[0], /'kilo'/);
|
||||
assert.match(chunks[0], /gsd install --kilo/);
|
||||
});
|
||||
|
||||
test('kilo: config OLDER than agents → no warning (boundary limit-1)', () => {
|
||||
const agentParent = path.join(tmpRoot, 'ko-config');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'kilo', configMtime: OLDER, agentDir, agentMtime: NEWER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: koEnv(agentParent) });
|
||||
assert.equal(wrote, false);
|
||||
assert.deepEqual(chunks, []);
|
||||
});
|
||||
|
||||
test('opencode: config OLDER than agents → no warning (boundary limit-1)', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: OLDER, agentDir, agentMtime: NEWER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) });
|
||||
assert.equal(wrote, false);
|
||||
@@ -253,7 +304,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('opencode: config EQUAL to agents → no warning (boundary limit)', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: NEWER, agentDir, agentMtime: NEWER });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) });
|
||||
assert.equal(wrote, false);
|
||||
@@ -272,7 +323,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('opencode: agent dir present but no gsd-* files → no warning', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: NEWER, agentDir, agentMtime: OLDER, agentFiles: ['some-other-agent.md'] });
|
||||
const wrote = warnIfStaleBake(tmpRoot, { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) });
|
||||
assert.equal(wrote, false);
|
||||
@@ -280,7 +331,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('dedup: second call with same (runtime, cwd) → no repeat warning', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: NEWER, agentDir, agentMtime: OLDER });
|
||||
const opts = { stderr: stderrStub, homedir: () => tmpRoot, env: ocEnv(agentParent) };
|
||||
const w1 = warnIfStaleBake(tmpRoot, opts);
|
||||
@@ -292,7 +343,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('global ~/.gsd/defaults.json (in tmp homedir) NEWER than agents → warning', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
const ap = path.join(agentDir, 'gsd-executor.md');
|
||||
fs.writeFileSync(ap, '---\n---\n');
|
||||
@@ -311,7 +362,7 @@ describe('stale-bake-guard.warnIfStaleBake (orchestrator, fixtures)', () => {
|
||||
|
||||
test('guard never throws — stderr.write failure is swallowed', () => {
|
||||
const agentParent = path.join(tmpRoot, 'oc-config');
|
||||
const agentDir = path.join(agentParent, 'agent');
|
||||
const agentDir = path.join(agentParent, 'agents');
|
||||
setupProject({ runtime: 'opencode', configMtime: NEWER, agentDir, agentMtime: OLDER });
|
||||
const throwingStderr = { write: () => { throw new Error('boom'); } };
|
||||
let threw = false;
|
||||
@@ -378,8 +429,8 @@ describe('stale-bake-guard parity with bin/install.js bake paths', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
test('STATIC_FRONTMATTER_RUNTIMES is exactly codex + opencode (no silent drift)', () => {
|
||||
assert.deepEqual([...STATIC_FRONTMATTER_RUNTIMES].sort(), ['codex', 'opencode']);
|
||||
test('STATIC_FRONTMATTER_RUNTIMES is exactly codex + kilo + opencode (no silent drift)', () => {
|
||||
assert.deepEqual([...STATIC_FRONTMATTER_RUNTIMES].sort(), ['codex', 'kilo', 'opencode']);
|
||||
});
|
||||
|
||||
test('opencode converter bakes a model: line when modelOverride is provided', () => {
|
||||
@@ -391,6 +442,23 @@ describe('stale-bake-guard parity with bin/install.js bake paths', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// #2093: kilo bakes model: the same way opencode does (same static-frontmatter constraint).
|
||||
test('kilo converter bakes a model: line when modelOverride is provided', () => {
|
||||
const sample = '---\nname: gsd-executor\ndescription: x\nmodel: sonnet\ntools: Read\n---\nbody\n';
|
||||
const out = install.convertClaudeToKiloFrontmatter(sample, { isAgent: true, modelOverride: 'anthropic/claude-sonnet-5' });
|
||||
assert.ok(
|
||||
typeof out === 'string' && out.includes('model: anthropic/claude-sonnet-5'),
|
||||
'kilo converter no longer bakes modelOverride — STATIC_FRONTMATTER_RUNTIMES is stale vs bin/install.js',
|
||||
);
|
||||
});
|
||||
|
||||
test('kilo converter omits model: when no override (stale-bake fallback shape)', () => {
|
||||
const sample = '---\nname: gsd-executor\ndescription: x\nmodel: sonnet\ntools: Read\n---\nbody\n';
|
||||
const out = install.convertClaudeToKiloFrontmatter(sample, { isAgent: true });
|
||||
assert.ok(typeof out === 'string', 'converter must return a string');
|
||||
assert.doesNotMatch(out, /^model:/m, 'no-override path should not emit a model: line');
|
||||
});
|
||||
|
||||
test('opencode converter omits model: when no override (stale-bake fallback shape)', () => {
|
||||
const sample = '---\nname: gsd-executor\ndescription: x\nmodel: sonnet\ntools: Read\n---\nbody\n';
|
||||
const out = install.convertClaudeToOpencodeFrontmatter(sample, { isAgent: true });
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"scan.md": 7732,
|
||||
"secure-phase.md": 13520,
|
||||
"session-report.md": 4044,
|
||||
"settings-advanced.md": 39728,
|
||||
"settings-advanced.md": 39908,
|
||||
"settings-integrations.md": 15892,
|
||||
"settings.md": 33467,
|
||||
"ship.md": 24691,
|
||||
|
||||
Reference in New Issue
Block a user