From 8c541b1f3a5448743f4e3ee60e0bd9dc141bfa59 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:09:40 +0200 Subject: [PATCH 01/30] test(01-02): add failing Kimi runtime selection coverage - Expect Kimi in interactive runtime option 11 - Move all-runtimes shortcut to option 17 - Cover Kimi in runtimeMap, allRuntimes and prompt text --- tests/multi-runtime-select.test.cjs | 71 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 1e15d046b..6ac4a58ed 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -61,38 +61,42 @@ describe('multi-runtime selection parsing', () => { }); test('single choice for kilo', () => { - assert.deepStrictEqual(parseRuntimeInput('11'), ['kilo']); + assert.deepStrictEqual(parseRuntimeInput('12'), ['kilo']); }); test('single choice for opencode', () => { - assert.deepStrictEqual(parseRuntimeInput('12'), ['opencode']); + assert.deepStrictEqual(parseRuntimeInput('13'), ['opencode']); }); test('single choice for qwen', () => { - assert.deepStrictEqual(parseRuntimeInput('13'), ['qwen']); + assert.deepStrictEqual(parseRuntimeInput('14'), ['qwen']); }); test('single choice for trae', () => { - assert.deepStrictEqual(parseRuntimeInput('14'), ['trae']); + assert.deepStrictEqual(parseRuntimeInput('15'), ['trae']); }); test('single choice for windsurf', () => { - assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']); + assert.deepStrictEqual(parseRuntimeInput('16'), ['windsurf']); }); - test('choice 16 returns all runtimes', () => { - assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); + test('single choice for kimi', () => { + assert.deepStrictEqual(parseRuntimeInput('11'), ['kimi']); }); - test('choice 16 returns all runtimes when mixed with separators or other tokens', () => { - // CR feedback: tokenized inputs that include 16 (e.g. trailing comma, or + test('choice 17 returns all runtimes', () => { + assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes); + }); + + test('choice 17 returns all runtimes when mixed with separators or other tokens', () => { + // CR feedback: tokenized inputs that include 17 (e.g. trailing comma, or // alongside other choices) must still expand to all-runtimes — previously - // only the bare "16" matched, so "16," or "16 1" silently installed a + // only the bare all-runtimes option matched, so "17," or "17 1" silently installed a // subset. - assert.deepStrictEqual(parseRuntimeInput('16,'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput('16 1'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput('1,16'), allRuntimes); - assert.deepStrictEqual(parseRuntimeInput(' 16 '), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('17,'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('17 1'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput('1,17'), allRuntimes); + assert.deepStrictEqual(parseRuntimeInput(' 17 '), allRuntimes); }); test('empty input defaults to claude', () => { @@ -101,13 +105,13 @@ describe('multi-runtime selection parsing', () => { }); test('invalid choices are ignored, falls back to claude if all invalid', () => { - assert.deepStrictEqual(parseRuntimeInput('17'), ['claude']); + assert.deepStrictEqual(parseRuntimeInput('18'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']); assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']); }); test('invalid choices mixed with valid are filtered out', () => { - assert.deepStrictEqual(parseRuntimeInput('1,17,7'), ['claude', 'copilot']); + assert.deepStrictEqual(parseRuntimeInput('1,18,7'), ['claude', 'copilot']); assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']); }); @@ -134,16 +138,17 @@ describe('install.js exports multi-select runtime metadata', () => { '8': 'cursor', '9': 'gemini', '10': 'hermes', - '11': 'kilo', - '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf', + '11': 'kimi', + '12': 'kilo', + '13': 'opencode', + '14': 'qwen', + '15': 'trae', + '16': 'windsurf', }; const expectedRuntimes = [ 'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', - 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', - 'trae', 'windsurf', + 'copilot', 'cursor', 'gemini', 'hermes', 'kimi', 'kilo', 'opencode', + 'qwen', 'trae', 'windsurf', ]; test('runtimeMap exports every option key bound to the right runtime', () => { @@ -160,20 +165,22 @@ describe('install.js exports multi-select runtime metadata', () => { 'allRuntimes has no duplicates'); }); - test('"All" shortcut (option 16) selects every runtime', () => { - assert.deepStrictEqual(parseRuntimeInput('16'), allRuntimes); + test('"All" shortcut (option 17) selects every runtime', () => { + assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes); }); - test('prompt lists Hermes Agent (10), Qwen Code (13), Trae (14), and All (16)', () => { + test('prompt lists Hermes Agent (10), Kimi (11), Qwen Code (14), Trae (15), and All (17)', () => { const prompt = stripAnsi(buildRuntimePromptText()); assert.ok(/\b10\)\s*Hermes Agent\b/.test(prompt), 'prompt lists Hermes Agent as option 10'); - assert.ok(/\b13\)\s*Qwen Code\b/.test(prompt), - 'prompt lists Qwen Code as option 13'); - assert.ok(/\b14\)\s*Trae\b/.test(prompt), - 'prompt lists Trae as option 14'); - assert.ok(/\b16\)\s*All\b/.test(prompt), - 'prompt lists All as option 16'); + assert.ok(/\b11\)\s*Kimi\b/.test(prompt), + 'prompt lists Kimi as option 11'); + assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt), + 'prompt lists Qwen Code as option 14'); + assert.ok(/\b15\)\s*Trae\b/.test(prompt), + 'prompt lists Trae as option 15'); + assert.ok(/\b17\)\s*All\b/.test(prompt), + 'prompt lists All as option 17'); }); test('prompt text shows multi-select hint', () => { From 0d456d4bb09e618a60bc231c09655a085687494b Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:11:56 +0200 Subject: [PATCH 02/30] test(01-03): add failing Kimi path and guard tests - Assert Kimi canonical skills path uses generic agents root - Assert Phase 1 layout placeholder and local install guard behavior --- .../bug-kimi-path-layout-local-guard.test.cjs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/bug-kimi-path-layout-local-guard.test.cjs diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs new file mode 100644 index 000000000..db636165b --- /dev/null +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -0,0 +1,141 @@ +'use strict'; + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const { cleanup } = require('./helpers.cjs'); +const { installerEnv } = require('./helpers/install-shared.cjs'); + +const ROOT = path.join(__dirname, '..'); +const INSTALL_SCRIPT = path.join(ROOT, 'bin', 'install.js'); + +const { + getGlobalConfigDir, + getGlobalSkillsBase, + getGlobalSkillDir, +} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs')); +const { + resolveRuntimeArtifactLayout, +} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs')); +const { + getGlobalDir, + getConfigDirFromHome, +} = require('../bin/install.js'); + +function withEnv(updates, fn) { + const saved = {}; + for (const key of Object.keys(updates)) { + saved[key] = process.env[key]; + const value = updates[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + try { + return fn(); + } finally { + for (const key of Object.keys(updates)) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + } +} + +describe('Kimi runtime homes', () => { + test('canonical global skills base is ~/.config/agents/skills, not ~/.kimi/skills', () => { + withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: undefined }, () => { + assert.strictEqual( + getGlobalConfigDir('kimi'), + path.join(os.homedir(), '.config', 'agents'), + ); + assert.strictEqual( + getGlobalSkillsBase('kimi'), + path.join(os.homedir(), '.config', 'agents', 'skills'), + ); + assert.strictEqual( + getGlobalSkillDir('kimi', 'gsd-help'), + path.join(os.homedir(), '.config', 'agents', 'skills', 'gsd-help'), + ); + assert.notStrictEqual( + getGlobalSkillsBase('kimi'), + path.join(os.homedir(), '.kimi', 'skills'), + ); + }); + }); + + test('KIMI_CONFIG_DIR overrides the Kimi generic agents config root', () => { + withEnv({ KIMI_CONFIG_DIR: '/tmp/custom-kimi-agents', XDG_CONFIG_HOME: undefined }, () => { + assert.strictEqual(getGlobalConfigDir('kimi'), '/tmp/custom-kimi-agents'); + assert.strictEqual( + getGlobalSkillsBase('kimi'), + path.join('/tmp/custom-kimi-agents', 'skills'), + ); + assert.strictEqual(getGlobalDir('kimi'), '/tmp/custom-kimi-agents'); + }); + }); + + test('XDG_CONFIG_HOME participates in the default Kimi generic agents path', () => { + withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/xdg-home' }, () => { + assert.strictEqual( + getGlobalConfigDir('kimi'), + path.join('/tmp/xdg-home', 'agents'), + ); + assert.strictEqual( + getConfigDirFromHome('kimi', true), + "'.config', 'agents'", + ); + }); + }); +}); + +describe('Kimi runtime artifact layout', () => { + test('known runtime with empty Phase 1 layout placeholder', () => { + const globalLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'global'); + assert.strictEqual(globalLayout.runtime, 'kimi'); + assert.strictEqual(globalLayout.configDir, '/tmp/kimi-config'); + assert.deepStrictEqual(globalLayout.kinds, []); + + const localLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'local'); + assert.strictEqual(localLayout.runtime, 'kimi'); + assert.deepStrictEqual(localLayout.kinds, []); + }); +}); + +describe('Kimi local install guard', () => { + test('--kimi --local exits successfully without writing local Kimi project artifacts', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-local-project-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-local-home-')); + try { + const result = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--local', '--no-sdk'], + { + cwd: tmpProject, + encoding: 'utf8', + env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }), + }, + ); + + assert.strictEqual( + result.status, + 0, + `expected --kimi --local guard to no-op successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, /Kimi local install/i); + assert.match(combined, /deferred/i); + + assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi')), 'must not create .kimi/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.agents')), 'must not create .agents/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.claude')), 'must not fall back to Claude local install'); + } finally { + cleanup(tmpProject); + cleanup(tmpHome); + } + }); +}); From 58626283b224c48f0f499c5266b3bcb801882845 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:17:56 +0200 Subject: [PATCH 03/30] feat(01-03): implement Kimi paths layout and local guard - Resolve Kimi global skills under the generic agents path - Add empty Kimi layout placeholder and local install no-op guard - Keep selection/path tests aligned without Kimi skill conversion --- bin/install.js | 145 ++++++++++++++++------------ src/runtime-artifact-layout.cts | 8 +- src/runtime-homes.cts | 10 ++ tests/helpers/install-shared.cjs | 1 + tests/install.test.cjs | 10 +- tests/multi-runtime-select.test.cjs | 16 ++- 6 files changed, 122 insertions(+), 68 deletions(-) diff --git a/bin/install.js b/bin/install.js index c143d8533..6664bab22 100755 --- a/bin/install.js +++ b/bin/install.js @@ -30,6 +30,8 @@ const { } = require(path.join(__dirname, '..', 'scripts', 'fix-slash-commands.cjs')); const { resolveAntigravityGlobalDir, + getGlobalConfigDir, + getGlobalSkillsBase, } = require('../gsd-core/bin/lib/runtime-homes.cjs'); /** @@ -239,23 +241,6 @@ const { const args = process.argv.slice(2); const hasGlobal = args.includes('--global') || args.includes('-g'); const hasLocal = args.includes('--local') || args.includes('-l'); -const hasOpencode = args.includes('--opencode'); -const hasClaude = args.includes('--claude'); -const hasGemini = args.includes('--gemini'); -const hasKilo = args.includes('--kilo'); -const hasCodex = args.includes('--codex'); -const hasCopilot = args.includes('--copilot'); -const hasAntigravity = args.includes('--antigravity'); -const hasCursor = args.includes('--cursor'); -const hasWindsurf = args.includes('--windsurf'); -const hasAugment = args.includes('--augment'); -const hasTrae = args.includes('--trae'); -const hasQwen = args.includes('--qwen'); -const hasHermes = args.includes('--hermes'); -const hasCodebuddy = args.includes('--codebuddy'); -const hasCline = args.includes('--cline'); -const hasBoth = args.includes('--both'); // Legacy flag, keeps working -const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); const hasSkillsRoot = args.includes('--skills-root'); const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1'; @@ -282,30 +267,37 @@ if (hasMinimal && _profileArgRaw) { process.exit(1); } -// Runtime selection - can be set by flags or interactive prompt -let selectedRuntimes = []; -if (hasAll) { - selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline']; -} else if (hasBoth) { - selectedRuntimes = ['claude', 'opencode']; -} else { - if (hasClaude) selectedRuntimes.push('claude'); - if (hasOpencode) selectedRuntimes.push('opencode'); - if (hasGemini) selectedRuntimes.push('gemini'); - if (hasKilo) selectedRuntimes.push('kilo'); - if (hasCodex) selectedRuntimes.push('codex'); - if (hasCopilot) selectedRuntimes.push('copilot'); - if (hasAntigravity) selectedRuntimes.push('antigravity'); - if (hasCursor) selectedRuntimes.push('cursor'); - if (hasWindsurf) selectedRuntimes.push('windsurf'); - if (hasAugment) selectedRuntimes.push('augment'); - if (hasTrae) selectedRuntimes.push('trae'); - if (hasQwen) selectedRuntimes.push('qwen'); - if (hasHermes) selectedRuntimes.push('hermes'); - if (hasCodebuddy) selectedRuntimes.push('codebuddy'); - if (hasCline) selectedRuntimes.push('cline'); +function selectRuntimesFromArgs(runtimeArgs) { + if (runtimeArgs.includes('--all')) { + return ['claude', 'kimi', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline']; + } + if (runtimeArgs.includes('--both')) { + return ['claude', 'opencode']; + } + + const selected = []; + if (runtimeArgs.includes('--claude')) selected.push('claude'); + if (runtimeArgs.includes('--opencode')) selected.push('opencode'); + if (runtimeArgs.includes('--gemini')) selected.push('gemini'); + if (runtimeArgs.includes('--kilo')) selected.push('kilo'); + if (runtimeArgs.includes('--codex')) selected.push('codex'); + if (runtimeArgs.includes('--copilot')) selected.push('copilot'); + if (runtimeArgs.includes('--antigravity')) selected.push('antigravity'); + if (runtimeArgs.includes('--cursor')) selected.push('cursor'); + if (runtimeArgs.includes('--windsurf')) selected.push('windsurf'); + if (runtimeArgs.includes('--augment')) selected.push('augment'); + if (runtimeArgs.includes('--trae')) selected.push('trae'); + if (runtimeArgs.includes('--qwen')) selected.push('qwen'); + if (runtimeArgs.includes('--hermes')) selected.push('hermes'); + if (runtimeArgs.includes('--kimi')) selected.push('kimi'); + if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy'); + if (runtimeArgs.includes('--cline')) selected.push('cline'); + return selected; } +// Runtime selection - can be set by flags or interactive prompt +let selectedRuntimes = selectRuntimesFromArgs(args); + // WSL + Windows Node.js detection // When Windows-native Node runs on WSL, os.homedir() and path.join() produce // backslash paths that don't resolve correctly on the Linux filesystem. @@ -354,6 +346,7 @@ function getDirName(runtime) { if (runtime === 'trae') return '.trae'; if (runtime === 'qwen') return '.qwen'; if (runtime === 'hermes') return '.hermes'; + if (runtime === 'kimi') return '.kimi'; if (runtime === 'codebuddy') return '.codebuddy'; if (runtime === 'cline') return '.cline'; return '.claude'; @@ -400,6 +393,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'hermes') return "'.hermes'"; if (runtime === 'codebuddy') return "'.codebuddy'"; if (runtime === 'cline') return "'.cline'"; + if (runtime === 'kimi') return "'.config', 'agents'"; return "'.claude'"; } @@ -605,6 +599,13 @@ function getGlobalDir(runtime, explicitDir = null) { return path.join(os.homedir(), '.cline'); } + if (runtime === 'kimi') { + if (explicitDir) { + return expandTilde(explicitDir); + } + return getGlobalConfigDir('kimi'); + } + // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { return expandTilde(explicitDir); @@ -8234,6 +8235,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { const isOpencode = runtime === 'opencode'; const isGemini = runtime === 'gemini'; const isKilo = runtime === 'kilo'; + const isKimi = runtime === 'kimi'; const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; const isAntigravity = runtime === 'antigravity'; @@ -8248,6 +8250,23 @@ function install(isGlobal, runtime = 'claude', options = {}) { const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); + if (isKimi && !isGlobal) { + console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 1.`); + console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); + console.log(` Use ${cyan}--kimi --global${reset} for the Phase 1 runtime skeleton.`); + return { + runtime, + skipped: true, + reason: 'kimi_local_deferred', + configDir: null, + settingsPath: null, + settings: null, + statuslineCommand: null, + updateBannerCommand: null, + rollbackInstallerMigrations: () => {}, + }; + } + // Reusable helper to copy hooks/lib/ (git-cmd.js + gsd-graphify-rebuild.sh). // Defined early so it is visible to both the main and Codex code paths. // `allowlist` (when non-empty) restricts copying to the named top-level entries, @@ -8374,6 +8393,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { if (isTrae) runtimeLabel = 'Trae'; if (isQwen) runtimeLabel = 'Qwen Code'; if (isHermes) runtimeLabel = 'Hermes Agent'; + if (isKimi) runtimeLabel = 'Kimi'; if (isCodebuddy) runtimeLabel = 'CodeBuddy'; if (isCline) runtimeLabel = 'Cline'; @@ -8657,7 +8677,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // handles per-runtime path + branding rewrites, including Qwen/Hermes. const _isSkillsRuntime = isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isAugment || isTrae || isCodebuddy || isQwen || isHermes || - (runtime === 'claude' && isGlobal); + isKimi || (runtime === 'claude' && isGlobal); if (_isSkillsRuntime) { // Layout-driven install for skills-based runtimes (full and minimal modes) @@ -8684,6 +8704,8 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('skills/gsd/*'); } + } else if (isKimi) { + console.log(` ${yellow}⚠${reset} Kimi SKILL.md conversion is deferred for Phase 2; no skills were written.`); } else { const skillsDir = path.join(targetDir, 'skills'); if (fs.existsSync(skillsDir)) { @@ -10348,14 +10370,15 @@ const runtimeMap = { '8': 'cursor', '9': 'gemini', '10': 'hermes', - '11': 'kilo', - '12': 'opencode', - '13': 'qwen', - '14': 'trae', - '15': 'windsurf' + '11': 'kimi', + '12': 'kilo', + '13': 'opencode', + '14': 'qwen', + '15': 'trae', + '16': 'windsurf' }; -const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; -const ALL_RUNTIMES_OPTION = '16'; +const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf']; +const ALL_RUNTIMES_OPTION = '17'; /** * Build the runtime-selection prompt text shown by the interactive installer. @@ -10373,12 +10396,13 @@ function buildRuntimePromptText() { ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset} ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset} ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset} - ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset} - ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset} - ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset} - ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} - ${cyan}16${reset}) All + ${cyan}11${reset}) Kimi ${dim}(~/.config/agents)${reset} + ${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset} + ${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset} + ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset} + ${cyan}15${reset}) Trae ${dim}(~/.trae)${reset} + ${cyan}16${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset} + ${cyan}17${reset}) All ${dim}Select multiple: 1,2,6 or 1 2 6${reset} `; @@ -10389,7 +10413,7 @@ function buildRuntimePromptText() { * Pure function — exported so tests can verify split/dedupe/fallback behavior. * - Accepts comma- and/or whitespace-separated choices * - Deduplicates while preserving order - * - Maps option 16 ("All") to every runtime + * - Maps option 17 ("All") to every runtime * - Falls back to ['claude'] when nothing valid is selected */ function parseRuntimeInput(answer) { @@ -10841,6 +10865,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { try { const printSummaries = () => { for (const result of results) { + if (result && result.skipped) continue; const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; finishInstall( result.settingsPath, @@ -11002,6 +11027,7 @@ module.exports = { maybeSuggestPathExport, runtimeMap, allRuntimes, + selectRuntimesFromArgs, GSD_UNINSTALL_HOOKS, parseRuntimeInput, buildRuntimePromptText, @@ -11047,12 +11073,11 @@ if (require.main === module && !process.env.GSD_TEST_MODE) { console.error('Usage: node install.js --skills-root '); process.exit(1); } - const globalDir = getGlobalDir(runtimeArg, null); - // Hermes nests GSD skills under skills/gsd/ as a single category (#2841). - // Other runtimes use a flat skills/ root. - const skillsRoot = runtimeArg === 'hermes' - ? path.join(globalDir, 'skills', 'gsd') - : path.join(globalDir, 'skills'); + const skillsRoot = getGlobalSkillsBase(runtimeArg); + if (skillsRoot === null) { + console.error(`${runtimeArg} does not use a skills directory`); + process.exit(1); + } console.log(skillsRoot); } else if (hasGlobal && hasLocal) { console.error(` ${yellow}Cannot specify both --global and --local${reset}`); diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index 0a0d836ac..b85544ea2 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -167,7 +167,7 @@ function findAgentsSourceRoot(runtimeConfigDir?: string): string { const ALLOWED_RUNTIMES = new Set([ 'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', - 'cline', 'opencode', 'kilo', + 'cline', 'kimi', 'opencode', 'kilo', ]); // --------------------------------------------------------------------------- @@ -304,6 +304,12 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: kinds = []; break; + case 'kimi': + // Phase 1 skeleton only: Kimi is recognized by the layout seam, but + // SKILL.md conversion and local project semantics are deferred. + kinds = []; + break; + case 'opencode': kinds = [commandsKind('command', 'gsd-', configDir)]; break; diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index be373e44f..a775726cf 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -14,6 +14,9 @@ * cline — Rules-based; commands are embedded in .clinerules. Cline does * not use a skills/ directory. getGlobalSkillDir() returns null * for cline so the caller can emit an appropriate warning. + * kimi — Agent Skills are discovered from the generic agents root: + * ~/.config/agents/skills. ~/.kimi/skills is compatible with + * Kimi CLI, but is not GSD's canonical Phase 1 install target. */ import os from 'node:os'; @@ -125,6 +128,13 @@ export function getGlobalConfigDir(runtime: string): string { case 'cline': return env['CLINE_CONFIG_DIR'] ? expandTilde(env['CLINE_CONFIG_DIR']) : path.join(home, '.cline'); + // ── Kimi CLI (generic agents XDG root) ───────────────────────────────── + case 'kimi': { + if (env['KIMI_CONFIG_DIR']) return expandTilde(env['KIMI_CONFIG_DIR']); + if (env['XDG_CONFIG_HOME']) return path.join(expandTilde(env['XDG_CONFIG_HOME']), 'agents'); + return path.join(home, '.config', 'agents'); + } + // ── OpenCode (XDG) ─────────────────────────────────────────────────────── case 'opencode': { if (env['OPENCODE_CONFIG_DIR']) return expandTilde(env['OPENCODE_CONFIG_DIR']); diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 14f131fbd..12c580d91 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -48,6 +48,7 @@ const RUNTIME_META = { cursor: { localDir: '.cursor', globalSuffix: '.cursor' }, gemini: { localDir: '.gemini', globalSuffix: '.gemini' }, hermes: { localDir: '.hermes', globalSuffix: '.hermes' }, + kimi: { localDir: '.kimi', globalSuffix: path.join('.config', 'agents') }, kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, diff --git a/tests/install.test.cjs b/tests/install.test.cjs index 3ac6e72c8..4b4fb2d39 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -649,17 +649,17 @@ describe('Kilo source integration assertions', () => { path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'update-context.cjs'), 'utf8'); test('--kilo flag parsing exists', () => { - assert.ok(src.includes("args.includes('--kilo')")); + assert.ok(src.includes("runtimeArgs.includes('--kilo')")); }); - test('runtimeMap has Kilo as option 11', () => { - assert.strictEqual(runtimeMap['11'], 'kilo'); + test('runtimeMap has Kilo as option 12 after Kimi', () => { + assert.strictEqual(runtimeMap['12'], 'kilo'); }); test('prompt text shows Kilo above OpenCode without marketing copy', () => { const plain = stripAnsi(buildRuntimePromptText()); - assert.ok(/\b11\)\s*Kilo\b/.test(plain)); - assert.ok(plain.indexOf('11) Kilo') < plain.indexOf('OpenCode')); + assert.ok(/\b12\)\s*Kilo\b/.test(plain)); + assert.ok(plain.indexOf('12) Kilo') < plain.indexOf('OpenCode')); assert.ok(!plain.includes('the #1 AI coding platform on OpenRouter')); }); diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 6ac4a58ed..8b2efa530 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -18,6 +18,7 @@ const assert = require('node:assert/strict'); const { runtimeMap, allRuntimes, + selectRuntimesFromArgs, parseRuntimeInput, buildRuntimePromptText, } = require('../bin/install.js'); @@ -48,7 +49,7 @@ describe('multi-runtime selection parsing', () => { test('space-separated choices return multiple runtimes', () => { assert.deepStrictEqual(parseRuntimeInput('1 7 9'), ['claude', 'copilot', 'gemini']); - assert.deepStrictEqual(parseRuntimeInput('8 11'), ['cursor', 'kilo']); + assert.deepStrictEqual(parseRuntimeInput('8 12'), ['cursor', 'kilo']); }); test('mixed comma and space separators work', () => { @@ -122,7 +123,7 @@ describe('multi-runtime selection parsing', () => { test('preserves selection order', () => { assert.deepStrictEqual(parseRuntimeInput('9,1,7'), ['gemini', 'claude', 'copilot']); - assert.deepStrictEqual(parseRuntimeInput('11,2,8'), ['kilo', 'antigravity', 'cursor']); + assert.deepStrictEqual(parseRuntimeInput('12,2,8'), ['kilo', 'antigravity', 'cursor']); }); }); @@ -169,6 +170,17 @@ describe('install.js exports multi-select runtime metadata', () => { assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes); }); + test('--kimi flag selects Kimi without interactive prompt', () => { + assert.deepStrictEqual(selectRuntimesFromArgs(['--kimi']), ['kimi']); + }); + + test('--all flag includes Kimi exactly once', () => { + const selected = selectRuntimesFromArgs(['--all']); + assert.ok(selected.includes('kimi'), '--all includes kimi'); + assert.strictEqual(selected.filter((runtime) => runtime === 'kimi').length, 1, + '--all includes kimi exactly once'); + }); + test('prompt lists Hermes Agent (10), Kimi (11), Qwen Code (14), Trae (15), and All (17)', () => { const prompt = stripAnsi(buildRuntimePromptText()); assert.ok(/\b10\)\s*Hermes Agent\b/.test(prompt), From 2a8745b21aa13f47cee5875e384ae6dec97cedc0 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:20:05 +0200 Subject: [PATCH 04/30] fix(01-03): guard Kimi global artifact writes - Keep Kimi global installs as a Phase 1 no-op skeleton - Assert no unconverted Kimi skills, agents, hooks, or payload files are written --- bin/install.js | 15 +++++--- .../bug-kimi-path-layout-local-guard.test.cjs | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/bin/install.js b/bin/install.js index 6664bab22..2cc6a09b2 100755 --- a/bin/install.js +++ b/bin/install.js @@ -8250,14 +8250,19 @@ function install(isGlobal, runtime = 'claude', options = {}) { const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); - if (isKimi && !isGlobal) { - console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 1.`); - console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); - console.log(` Use ${cyan}--kimi --global${reset} for the Phase 1 runtime skeleton.`); + if (isKimi) { + const scopeLabel = isGlobal ? 'global' : 'local'; + console.log(` ${yellow}⚠${reset} Kimi ${scopeLabel} install is deferred for Phase 1.`); + if (isGlobal) { + console.log(` No Kimi skills, agents, hooks, or workflow payload artifacts were written.`); + } else { + console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); + } + console.log(` Phase 1 only registers Kimi selection, paths, and layout guards.`); return { runtime, skipped: true, - reason: 'kimi_local_deferred', + reason: isGlobal ? 'kimi_global_deferred' : 'kimi_local_deferred', configDir: null, settingsPath: null, settings: null, diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index db636165b..5fad93f0b 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -138,4 +138,39 @@ describe('Kimi local install guard', () => { cleanup(tmpHome); } }); + + test('--kimi --global exits successfully without writing unconverted Kimi artifacts', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-project-')); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-home-')); + try { + const result = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'], + { + cwd: tmpProject, + encoding: 'utf8', + env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }), + }, + ); + + assert.strictEqual( + result.status, + 0, + `expected --kimi --global skeleton guard to no-op successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, /Kimi global install/i); + assert.match(combined, /deferred/i); + + assert.ok(!fs.existsSync(path.join(tmpConfig, 'skills')), 'must not write unconverted Kimi skills'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write unconverted Kimi agents'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root'); + } finally { + cleanup(tmpProject); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); }); From d008c83ec910bdc2089cfc5735efb862b6825a76 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:21:34 +0200 Subject: [PATCH 05/30] feat(01-02): add Kimi runtime name policy - Register canonical kimi in runtime alias manifest and fallback policy - Add focused canonicalization coverage without extra aliases --- .../bin/shared/runtime-aliases.manifest.json | 3 +++ src/runtime-name-policy.cts | 1 + tests/runtime-name-policy.test.cjs | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/runtime-name-policy.test.cjs diff --git a/gsd-core/bin/shared/runtime-aliases.manifest.json b/gsd-core/bin/shared/runtime-aliases.manifest.json index 2af00c148..3192f64ce 100644 --- a/gsd-core/bin/shared/runtime-aliases.manifest.json +++ b/gsd-core/bin/shared/runtime-aliases.manifest.json @@ -64,6 +64,9 @@ "hermes-agent", "hermes-cli" ], + "kimi": [ + "kimi" + ], "codebuddy": [ "codebuddy", "codebuddy-cli" diff --git a/src/runtime-name-policy.cts b/src/runtime-name-policy.cts index 199515482..68cb1a850 100644 --- a/src/runtime-name-policy.cts +++ b/src/runtime-name-policy.cts @@ -28,6 +28,7 @@ const FALLBACK_ALIASES: Readonly> = { trae: ['trae', 'trae-cli'], qwen: ['qwen', 'qwen-code', 'qwen-cli'], hermes: ['hermes', 'hermes-agent', 'hermes-cli'], + kimi: ['kimi'], codebuddy: ['codebuddy', 'codebuddy-cli'], cline: ['cline', 'cline-cli'], }; diff --git a/tests/runtime-name-policy.test.cjs b/tests/runtime-name-policy.test.cjs new file mode 100644 index 000000000..abc6ee11d --- /dev/null +++ b/tests/runtime-name-policy.test.cjs @@ -0,0 +1,20 @@ +'use strict'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); +const { + canonicalizeRuntimeName, + resolveRuntimeNameFromCandidates, +} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs')); + +describe('runtime-name-policy canonical runtime ids', () => { + test('canonicalizes Kimi without adding extra aliases', () => { + assert.strictEqual(canonicalizeRuntimeName('kimi'), 'kimi'); + assert.strictEqual(canonicalizeRuntimeName(' KIMI '), 'kimi'); + assert.strictEqual(resolveRuntimeNameFromCandidates('', null, 'kimi'), 'kimi'); + assert.strictEqual(canonicalizeRuntimeName('kimi-cli'), null); + }); +}); From 6a6243331e54599edcceb4e4998379daec6c9e57 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:49:06 +0200 Subject: [PATCH 06/30] test(02-01): add failing Kimi skill converter tests - Cover Kimi SKILL.md frontmatter shape - Cover /skill:gsd-new-project invocation rewrites - Assert Phase 3 agent/tool artifacts are absent --- tests/kimi-skill-converter.test.cjs | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/kimi-skill-converter.test.cjs diff --git a/tests/kimi-skill-converter.test.cjs b/tests/kimi-skill-converter.test.cjs new file mode 100644 index 000000000..3d07f6b96 --- /dev/null +++ b/tests/kimi-skill-converter.test.cjs @@ -0,0 +1,94 @@ +/** + * Kimi CLI skill converter tests. + * + * Kimi Agent Skills use gsd-/SKILL.md directories with lowercase + * hyphenated frontmatter names and /skill: invocation syntax. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + convertClaudeCommandToKimiSkill, +} = require('../bin/install.js'); + +function sampleCommand() { + return [ + '---', + 'name: gsd:new-project', + 'description: Create a new GSD project', + 'allowed-tools:', + ' - Read', + ' - Bash', + 'agent: gsd-planner', + 'tools: Read, Bash', + 'model: opus', + 'color: blue', + 'skills:', + ' - gsd-planner-workflow', + 'hooks:', + ' PostToolUse:', + ' - matcher: "Write|Edit"', + '---', + '', + 'Invoke /gsd:new-project from slash form.', + 'Invoke gsd:new-project from bare colon form.', + 'Invoke /gsd-new-project from hyphen slash form.', + 'Invoke $gsd-new-project from shell-style form.', + 'Do not rewrite /gsd-tools, gsd-new-projector, or $gsd-planets.', + ].join('\n'); +} + +describe('convertClaudeCommandToKimiSkill', () => { + test('emits Kimi SKILL.md frontmatter with name and description only', () => { + const result = convertClaudeCommandToKimiSkill(sampleCommand(), 'gsd-new-project'); + + assert.ok(result.startsWith('---\n'), 'frontmatter starts with ---'); + assert.ok(result.includes('\n---\n'), 'frontmatter closes with ---'); + assert.ok(result.includes('name: gsd-new-project'), 'frontmatter name uses Kimi-safe hyphen form'); + assert.ok( + result.includes('description: "Create a new GSD project"'), + 'description is preserved from source frontmatter' + ); + + for (const unsupported of [ + 'allowed-tools', + 'agent', + 'tools', + 'model', + 'color', + 'skills', + 'hooks', + ]) { + assert.ok(!result.includes(`${unsupported}:`), `${unsupported}: is stripped from Kimi frontmatter`); + } + }); + + test('rewrites GSD command references to Kimi skill invocation syntax', () => { + const result = convertClaudeCommandToKimiSkill(sampleCommand(), 'gsd-new-project'); + + assert.equal( + (result.match(/\/skill:gsd-new-project/g) || []).length, + 4, + 'all supported source invocation forms are rewritten' + ); + assert.ok(!result.includes('/gsd:new-project'), 'slash colon form is removed'); + assert.ok(!result.includes('gsd:new-project'), 'bare colon form is removed'); + assert.ok(!result.includes('/gsd-new-project'), 'slash hyphen form is removed'); + assert.ok(!result.includes('$gsd-new-project'), 'shell-style form is removed'); + }); + + test('does not rewrite unrelated words or introduce Phase 3 artifacts', () => { + const result = convertClaudeCommandToKimiSkill(sampleCommand(), 'gsd-new-project'); + + assert.ok(result.includes('/gsd-tools'), 'non-command slash token is preserved'); + assert.ok(result.includes('gsd-new-projector'), 'longer hyphenated word is preserved'); + assert.ok(result.includes('$gsd-planets'), 'unrelated shell-style token is preserved'); + assert.ok(!result.includes('type: flow'), 'Phase 2 does not create Kimi flow skills'); + assert.ok(!result.includes('kimi_cli.tools'), 'Phase 2 does not emit Kimi tool module paths'); + assert.ok(!result.includes('system_prompt_path'), 'Phase 2 does not emit custom agent YAML'); + assert.ok(!result.includes('version: 1'), 'Phase 2 does not emit Kimi agent YAML markers'); + }); +}); From ca682136646843cfbe29db8376cb199c9eb27ba6 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:50:58 +0200 Subject: [PATCH 07/30] feat(02-01): implement Kimi skill converter - Export convertClaudeCommandToKimiSkill from install.js - Emit Kimi-safe SKILL.md frontmatter - Rewrite GSD command references to /skill:gsd-* --- bin/install.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/bin/install.js b/bin/install.js index 2cc6a09b2..7e560cae5 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2141,6 +2141,47 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c return `${fm}\n${normalizedBody}`; } +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function normalizeKimiSkillName(skillName) { + let text = String(skillName || '').trim().toLowerCase(); + if (text.startsWith('/')) text = text.slice(1); + if (text.startsWith('$')) text = text.slice(1); + text = text.replace(/^gsd:/, 'gsd-'); + if (!text.startsWith('gsd-')) text = `gsd-${text}`; + text = text.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + return text || 'gsd-command'; +} + +function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) { + if (!Array.isArray(cmdNames) || cmdNames.length === 0) return content; + const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp); + const commandGroup = commands.join('|'); + const colonPattern = new RegExp(`(? `/skill:gsd-${cmd}`) + .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`); +} + +function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) { + const { frontmatter, body } = extractFrontmatterAndBody(content); + const kimiSkillName = normalizeKimiSkillName(skillName); + const names = cmdNames || readGsdCommandNames(); + const description = frontmatter + ? extractFrontmatterField(frontmatter, 'description') || `Run GSD workflow ${kimiSkillName}.` + : `Run GSD workflow ${kimiSkillName}.`; + const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations( + frontmatter ? body : content, + names + ); + + return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\n${normalizedBody}`; +} + /** * Convert a Claude agent (.md) to a Copilot agent (.agent.md). * Applies tool mapping + deduplication, formats tools as JSON array. @@ -10979,6 +11020,7 @@ module.exports = { installAllRuntimes, uninstall, convertClaudeCommandToCodexSkill, + convertClaudeCommandToKimiSkill, convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, configureOpencodePermissions, From da1b6111e7fe2d76f9f4e8297a6f49903ae6af94 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:59:55 +0200 Subject: [PATCH 08/30] test(02-02): add failing Kimi global skill install tests - Expect Kimi global layout to stage skills/gsd-*/SKILL.md - Assert --kimi --global writes gsd-new-project SKILL.md - Preserve explicit --kimi --local no-op guard --- .../bug-kimi-path-layout-local-guard.test.cjs | 26 +++++++++----- tests/install-runtime-artifacts.test.cjs | 13 +++++-- tests/runtime-artifact-layout.test.cjs | 35 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 5fad93f0b..2846e82b7 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -94,11 +94,15 @@ describe('Kimi runtime homes', () => { }); describe('Kimi runtime artifact layout', () => { - test('known runtime with empty Phase 1 layout placeholder', () => { + test('global layout stages Kimi skills while local layout remains guarded', () => { const globalLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'global'); assert.strictEqual(globalLayout.runtime, 'kimi'); assert.strictEqual(globalLayout.configDir, '/tmp/kimi-config'); - assert.deepStrictEqual(globalLayout.kinds, []); + assert.strictEqual(globalLayout.kinds.length, 1); + assert.strictEqual(globalLayout.kinds[0].kind, 'skills'); + assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills'); + assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-'); + assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function'); const localLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'local'); assert.strictEqual(localLayout.runtime, 'kimi'); @@ -139,7 +143,7 @@ describe('Kimi local install guard', () => { } }); - test('--kimi --global exits successfully without writing unconverted Kimi artifacts', () => { + test('--kimi --global writes converted Kimi skills only', () => { const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-project-')); const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-config-')); const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-home-')); @@ -157,14 +161,20 @@ describe('Kimi local install guard', () => { assert.strictEqual( result.status, 0, - `expected --kimi --global skeleton guard to no-op successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + `expected --kimi --global to install Kimi skills successfully\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); const combined = `${result.stdout}\n${result.stderr}`; - assert.match(combined, /Kimi global install/i); - assert.match(combined, /deferred/i); + assert.match(combined, /Installing for .*Kimi/i); + assert.match(combined, /Installed \d+ skills to skills\//i); - assert.ok(!fs.existsSync(path.join(tmpConfig, 'skills')), 'must not write unconverted Kimi skills'); - assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write unconverted Kimi agents'); + const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); + assert.ok(fs.existsSync(skillFile), 'must write gsd-new-project/SKILL.md'); + const skillContent = fs.readFileSync(skillFile, 'utf8'); + assert.match(skillContent, /^name: gsd-new-project$/m); + assert.match(skillContent, /\/skill:gsd-new-project/); + assert.doesNotMatch(skillContent, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); + + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write Kimi agents'); assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts'); assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root'); } finally { diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 76fd3d566..e5e867f7d 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -49,13 +49,13 @@ const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST }); const SKILLS_RUNTIMES_LAYOUT = [ 'claude', 'cursor', 'codex', 'copilot', 'antigravity', - 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', + 'windsurf', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy', ]; const ALL_RUNTIMES_LAYOUT = [ 'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', - 'cline', 'opencode', 'kilo', + 'cline', 'kimi', 'opencode', 'kilo', ]; function countPrefixedEntries(destDir, prefix) { @@ -94,6 +94,15 @@ describe('installRuntimeArtifacts — skills runtimes write gsd-prefixed skill d `${runtime}: ${skillsKind.prefix}help/SKILL.md must exist` ); + if (runtime === 'kimi') { + const newProjectSkill = path.join(destDir, 'gsd-new-project', 'SKILL.md'); + assert.ok(fs.existsSync(newProjectSkill), 'kimi: gsd-new-project/SKILL.md must exist'); + const content = fs.readFileSync(newProjectSkill, 'utf8'); + assert.match(content, /^name: gsd-new-project$/m); + assert.match(content, /\/skill:gsd-new-project/); + assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); + } + if (RESOLVED_CORE.skills !== '*') { const prefixedCount = countPrefixedEntries(destDir, skillsKind.prefix || 'gsd-'); assert.strictEqual(prefixedCount, RESOLVED_CORE.skills.size, diff --git a/tests/runtime-artifact-layout.test.cjs b/tests/runtime-artifact-layout.test.cjs index 01c72657e..415b70c3e 100644 --- a/tests/runtime-artifact-layout.test.cjs +++ b/tests/runtime-artifact-layout.test.cjs @@ -175,6 +175,24 @@ describe('resolveRuntimeArtifactLayout — qwen', () => { }); }); +describe('resolveRuntimeArtifactLayout — kimi', () => { + test('returns global skills layout and guarded empty local layout for kimi', () => { + const globalLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global'); + assert.strictEqual(globalLayout.runtime, 'kimi'); + assert.strictEqual(globalLayout.configDir, FAKE_DIR); + assert.strictEqual(globalLayout.kinds.length, 1); + assert.strictEqual(globalLayout.kinds[0].kind, 'skills'); + assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills'); + assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-'); + assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function'); + + const localLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'local'); + assert.strictEqual(localLayout.runtime, 'kimi'); + assert.strictEqual(localLayout.configDir, FAKE_DIR); + assert.deepStrictEqual(localLayout.kinds, []); + }); +}); + describe('resolveRuntimeArtifactLayout — hermes', () => { test('returns correct layout for hermes', () => { const layout = resolveRuntimeArtifactLayout('hermes', FAKE_DIR); @@ -379,6 +397,23 @@ describe('stage — skills kind (claude global)', () => { }); }); +describe('stage — skills kind (kimi global)', () => { + test('stage returns Kimi SKILL.md dirs with /skill:gsd-* invocations', () => { + const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global'); + const skillsKind = layout.kinds.find(k => k.kind === 'skills'); + assert.ok(skillsKind, 'should have a skills kind'); + + const stagedDir = skillsKind.stage(PROFILE_CORE); + assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist'); + const skillMd = path.join(stagedDir, 'gsd-new-project', 'SKILL.md'); + assert.ok(fs.existsSync(skillMd), 'gsd-new-project/SKILL.md must exist'); + const content = fs.readFileSync(skillMd, 'utf8'); + assert.match(content, /^name: gsd-new-project$/m); + assert.match(content, /\/skill:gsd-new-project/); + assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); + }); +}); + describe('stage — opencode commands kind', () => { test('opencode stage returns directory with .md files for selected skills', () => { const layout = resolveRuntimeArtifactLayout('opencode', FAKE_STAGE_DIR); From cc447fcd6685ff7bb99a8480f30a78eb3f9318c9 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:03:07 +0200 Subject: [PATCH 09/30] feat(02-02): install Kimi global skills only - Wire Kimi global layout to convertClaudeCommandToKimiSkill - Keep --kimi --local guarded as a no-op - Add Kimi self-invocation hint without agent or tool artifacts --- bin/install.js | 42 +++++++++++++++++++++-------- src/runtime-artifact-layout.cts | 6 ++--- tests/kimi-skill-converter.test.cjs | 4 +-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/bin/install.js b/bin/install.js index 7e560cae5..d2cbe2b6a 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2179,7 +2179,7 @@ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cm names ); - return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\n${normalizedBody}`; + return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`; } /** @@ -8291,19 +8291,14 @@ function install(isGlobal, runtime = 'claude', options = {}) { const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); - if (isKimi) { - const scopeLabel = isGlobal ? 'global' : 'local'; - console.log(` ${yellow}⚠${reset} Kimi ${scopeLabel} install is deferred for Phase 1.`); - if (isGlobal) { - console.log(` No Kimi skills, agents, hooks, or workflow payload artifacts were written.`); - } else { - console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); - } - console.log(` Phase 1 only registers Kimi selection, paths, and layout guards.`); + if (isKimi && !isGlobal) { + console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`); + console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); + console.log(` Project-level Kimi install semantics remain deferred.`); return { runtime, skipped: true, - reason: isGlobal ? 'kimi_global_deferred' : 'kimi_local_deferred', + reason: 'kimi_local_deferred', configDir: null, settingsPath: null, settings: null, @@ -8455,6 +8450,31 @@ function install(isGlobal, runtime = 'claude', options = {}) { rollback(); }; + if (isKimi && isGlobal) { + installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); + const skillsDir = path.join(targetDir, 'skills'); + const count = fs.existsSync(skillsDir) + ? fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length + : 0; + if (count > 0) { + console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); + } else { + throw new Error('Kimi global install produced no skills/gsd-* entries'); + } + return { + runtime, + skipped: true, + reason: 'kimi_global_skills_only', + configDir: targetDir, + settingsPath: null, + settings: null, + statuslineCommand: null, + updateBannerCommand: null, + rollbackInstallerMigrations, + }; + } + // Save any locally modified GSD files before they get wiped. // The pristine context lets saveLocalPatches populate gsd-pristine/ via // the install transform pipeline, giving the reapply-patches Step 5 diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index b85544ea2..dfe874a87 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -305,9 +305,9 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: break; case 'kimi': - // Phase 1 skeleton only: Kimi is recognized by the layout seam, but - // SKILL.md conversion and local project semantics are deferred. - kinds = []; + kinds = scope === 'global' + ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir)] + : []; break; case 'opencode': diff --git a/tests/kimi-skill-converter.test.cjs b/tests/kimi-skill-converter.test.cjs index 3d07f6b96..dc5974321 100644 --- a/tests/kimi-skill-converter.test.cjs +++ b/tests/kimi-skill-converter.test.cjs @@ -71,8 +71,8 @@ describe('convertClaudeCommandToKimiSkill', () => { assert.equal( (result.match(/\/skill:gsd-new-project/g) || []).length, - 4, - 'all supported source invocation forms are rewritten' + 5, + 'self invocation hint and all supported source invocation forms are emitted' ); assert.ok(!result.includes('/gsd:new-project'), 'slash colon form is removed'); assert.ok(!result.includes('gsd:new-project'), 'bare colon form is removed'); From 3541e4877d777d8ce715b9b48ed61e78761d01c7 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:32:08 +0200 Subject: [PATCH 10/30] test(03-01): add failing Kimi agent contract tests - cover root Kimi custom agent artifact shape - cover subagent prompt separation and diagnostics --- tests/kimi-agent-converter.test.cjs | 137 ++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/kimi-agent-converter.test.cjs diff --git a/tests/kimi-agent-converter.test.cjs b/tests/kimi-agent-converter.test.cjs new file mode 100644 index 000000000..e7cdb5bda --- /dev/null +++ b/tests/kimi-agent-converter.test.cjs @@ -0,0 +1,137 @@ +/** + * Kimi CLI agent artifact contract tests. + * + * Kimi custom agents are explicit YAML files loaded with `kimi --agent-file`. + * This suite tests the in-memory artifact contract only; install/layout wiring + * belongs to the later Phase 3 slices. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + buildKimiAgentArtifacts, +} = require('../bin/install.js'); + +const ROOT_AGENT = `--- +name: gsd +description: Root GSD agent for Kimi CLI +tools: Agent, mcp__github__search, DefinitelyUnknownTool +color: blue +--- + +# GSD Root + +Coordinate GSD workflows through subagents. +Read ~/.claude/gsd-core when source-package context is needed.`; + +const EXECUTOR_AGENT = `--- +name: gsd-executor +description: Execute planned GSD task slices with atomic commits. +tools: Read, Write, Edit, Bash, Grep, Glob +color: yellow +--- + + +You are a GSD plan executor. +`; + +const INVALID_AGENT = `--- +name: not a valid kimi agent +description: Invalid name should be diagnosed and skipped. +tools: Agent +--- + +This should not become a Kimi custom subagent.`; + +describe('buildKimiAgentArtifacts', () => { + test('builds a root Kimi agent YAML contract with explicit subagent paths', () => { + const result = buildKimiAgentArtifacts({ + rootAgent: ROOT_AGENT, + subagents: [ + { path: 'agents/gsd-executor.md', content: EXECUTOR_AGENT }, + ], + requestedSubagents: ['gsd-executor', 'gsd-missing'], + }); + + assert.equal(result.root.yamlPath, 'agents/gsd.yaml'); + assert.equal(result.root.promptPath, 'agents/gsd.md'); + assert.ok(result.root.yaml.includes('version: 1'), 'root YAML includes Kimi version marker'); + assert.ok(result.root.yaml.includes('agent:'), 'root YAML includes agent object'); + assert.ok(result.root.yaml.includes('name: gsd'), 'root agent name is gsd'); + assert.ok(result.root.yaml.includes('extend: default'), 'root agent extends default Kimi behavior'); + assert.ok(result.root.yaml.includes('system_prompt_path: ./gsd.md'), 'root prompt path is relative'); + assert.ok(result.root.yaml.includes('tools:'), 'root YAML has a tools field'); + assert.ok(result.root.yaml.includes('kimi_cli.tools.agent:Agent'), 'root can call Kimi subagents'); + assert.ok(result.root.yaml.includes('subagents:'), 'root YAML declares custom subagents'); + assert.ok(result.root.yaml.includes('gsd-executor:'), 'known GSD subagent key is canonical'); + assert.ok( + result.root.yaml.includes('path: ./subagents/gsd-executor.yaml'), + 'known GSD subagent path is relative to root YAML' + ); + assert.ok(!result.root.yaml.includes('gsd-missing'), 'unknown requested subagent is excluded'); + }); + + test('emits separate frontmatter-free Markdown prompts for root and subagents', () => { + const result = buildKimiAgentArtifacts({ + rootAgent: ROOT_AGENT, + subagents: [ + { path: 'agents/gsd-executor.md', content: EXECUTOR_AGENT }, + ], + requestedSubagents: ['gsd-executor'], + }); + + const executor = result.subagents.find((artifact) => artifact.name === 'gsd-executor'); + assert.ok(executor, 'executor subagent artifact exists'); + + assert.equal(executor.yamlPath, 'agents/subagents/gsd-executor.yaml'); + assert.equal(executor.promptPath, 'agents/subagents/gsd-executor.md'); + assert.ok(executor.yaml.includes('system_prompt_path: ./gsd-executor.md')); + + for (const prompt of [result.root.prompt, executor.prompt]) { + assert.ok(!prompt.trimStart().startsWith('---'), 'source frontmatter is removed'); + assert.ok(!prompt.includes('tools:'), 'source frontmatter tools do not leak into prompt'); + assert.ok(!prompt.includes('color:'), 'source frontmatter color does not leak into prompt'); + } + assert.ok(result.root.prompt.includes('# GSD Root'), 'root body content is preserved'); + assert.ok(executor.prompt.includes('You are a GSD plan executor.'), 'subagent body content is preserved'); + assert.ok(!result.root.prompt.includes('~/.claude/gsd-core'), 'Claude-specific path is neutralized'); + }); + + test('diagnoses unknown subagents and unsupported inputs instead of emitting invalid names', () => { + const result = buildKimiAgentArtifacts({ + rootAgent: ROOT_AGENT, + subagents: [ + { path: 'agents/gsd-executor.md', content: EXECUTOR_AGENT }, + { path: 'agents/not-valid.md', content: INVALID_AGENT }, + ], + requestedSubagents: ['gsd-executor', 'gsd-missing'], + }); + + assert.deepEqual( + result.subagents.map((artifact) => artifact.name), + ['gsd-executor'], + 'invalid and unknown subagents are not emitted' + ); + assert.ok( + result.diagnostics.some((item) => item.code === 'kimi_unknown_subagent' && item.value === 'gsd-missing'), + 'unknown requested subagent is diagnosed' + ); + assert.ok( + result.diagnostics.some((item) => item.code === 'kimi_invalid_subagent_name'), + 'invalid source subagent name is diagnosed' + ); + assert.ok( + result.diagnostics.some((item) => item.code === 'kimi_mcp_tool_excluded'), + 'MCP-managed tools are diagnosed and excluded' + ); + assert.ok( + result.diagnostics.some((item) => item.code === 'kimi_unsupported_tool'), + 'unsupported tools are diagnosed and excluded' + ); + assert.ok(!result.root.yaml.includes('mcp__github__search'), 'MCP tool names are not emitted'); + assert.ok(!result.root.yaml.includes('DefinitelyUnknownTool'), 'unsupported tool names are not emitted'); + }); +}); From f596839284c65312b0b880af9e84213633286d1a Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:34:57 +0200 Subject: [PATCH 11/30] feat(03-01): implement Kimi agent artifact contract - add root and subagent YAML artifact builder - return diagnostics for unknown subagents and excluded tools --- bin/install.js | 279 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/bin/install.js b/bin/install.js index d2cbe2b6a..0ea212dc9 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2182,6 +2182,284 @@ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cm return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`; } +const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/; +const kimiAgentContractToolMap = { + Agent: 'kimi_cli.tools.agent:Agent', + Task: 'kimi_cli.tools.agent:Agent', +}; + +function parseKimiAgentSource(source) { + if (typeof source === 'string') { + return { + path: null, + content: source, + }; + } + if (!source || typeof source !== 'object' || typeof source.content !== 'string') { + return null; + } + return { + path: typeof source.path === 'string' ? source.path : null, + content: source.content, + }; +} + +function parseFrontmatterTools(frontmatter) { + if (!frontmatter) return []; + const lines = frontmatter.split(/\r?\n/); + const tools = []; + let collecting = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + if (collecting) { + if (trimmed.startsWith('- ')) { + tools.push(trimmed.slice(2).trim()); + continue; + } + collecting = false; + } + + if (trimmed === 'tools:' || trimmed === 'allowed-tools:') { + collecting = true; + continue; + } + + if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) { + const value = trimmed.slice(trimmed.indexOf(':') + 1).trim(); + if (value) { + for (const tool of value.split(',')) { + const name = tool.trim(); + if (name) tools.push(name); + } + } else { + collecting = true; + } + } + } + + return tools; +} + +function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) { + diagnostics.push({ + level: 'warning', + code, + message, + value, + source, + }); +} + +function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) { + const mapped = []; + const seen = new Set(); + + for (const rawTool of toolNames) { + const tool = String(rawTool || '').trim(); + if (!tool) continue; + + if (tool.startsWith('mcp__')) { + addKimiAgentDiagnostic( + diagnostics, + 'kimi_mcp_tool_excluded', + `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.`, + tool, + sourceName + ); + continue; + } + + const kimiTool = kimiAgentContractToolMap[tool]; + if (!kimiTool) { + addKimiAgentDiagnostic( + diagnostics, + 'kimi_unsupported_tool', + `Tool '${tool}' is not part of the Phase 3 Kimi agent contract mapper.`, + tool, + sourceName + ); + continue; + } + + if (!seen.has(kimiTool)) { + seen.add(kimiTool); + mapped.push(kimiTool); + } + } + + return mapped; +} + +function neutralizeKimiAgentPrompt(content) { + const { frontmatter, body } = extractFrontmatterAndBody(content); + let prompt = frontmatter ? body : content; + prompt = neutralizeAgentReferences(prompt, 'AGENTS.md'); + prompt = prompt.replace(/~\/\.claude\/gsd-core\b/g, 'GSD core'); + prompt = prompt.replace(/\$HOME\/\.claude\/gsd-core\b/g, 'GSD core'); + return prompt.replace(/^\s*\r?\n/, ''); +} + +function pushKimiToolsYaml(lines, indent, tools) { + const prefix = ' '.repeat(indent); + if (!Array.isArray(tools) || tools.length === 0) { + lines.push(`${prefix}tools: []`); + return; + } + lines.push(`${prefix}tools:`); + for (const tool of tools) { + lines.push(`${prefix} - ${yamlQuote(tool)}`); + } +} + +function buildKimiRootAgentYaml({ description, tools, subagents }) { + const lines = [ + 'version: 1', + 'agent:', + ' name: gsd', + ` description: ${yamlQuote(toSingleLine(description || 'Run GSD workflows in Kimi CLI.'))}`, + ' extend: default', + ' system_prompt_path: ./gsd.md', + ]; + pushKimiToolsYaml(lines, 2, tools); + + if (subagents.length > 0) { + lines.push(' subagents:'); + for (const subagent of subagents) { + lines.push(` ${subagent.name}:`); + lines.push(` path: ./subagents/${subagent.name}.yaml`); + lines.push(` description: ${yamlQuote(toSingleLine(subagent.description))}`); + } + } + + return `${lines.join('\n')}\n`; +} + +function buildKimiSubagentYaml({ name, description, tools }) { + const lines = [ + 'version: 1', + 'agent:', + ` name: ${name}`, + ` description: ${yamlQuote(toSingleLine(description || `Run ${name}.`))}`, + ` system_prompt_path: ./${name}.md`, + ]; + pushKimiToolsYaml(lines, 2, tools); + return `${lines.join('\n')}\n`; +} + +function buildKimiAgentArtifacts({ + rootAgent = '', + subagents = [], + requestedSubagents = null, +} = {}) { + const diagnostics = []; + const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' }; + const { frontmatter: rootFrontmatter } = extractFrontmatterAndBody(rootSource.content); + const rootDescription = rootFrontmatter + ? extractFrontmatterField(rootFrontmatter, 'description') || 'Run GSD workflows in Kimi CLI.' + : 'Run GSD workflows in Kimi CLI.'; + + const subagentSources = Array.isArray(subagents) ? subagents : []; + if (!Array.isArray(subagents)) { + addKimiAgentDiagnostic( + diagnostics, + 'kimi_unsupported_subagents_input', + 'Subagents input must be an array of Markdown strings or source objects.', + typeof subagents, + null + ); + } + + const subagentMap = new Map(); + for (const source of subagentSources) { + const parsed = parseKimiAgentSource(source); + if (!parsed) { + addKimiAgentDiagnostic( + diagnostics, + 'kimi_unsupported_subagent_input', + 'Subagent source must be a Markdown string or an object with content.', + typeof source, + null + ); + continue; + } + + const { frontmatter } = extractFrontmatterAndBody(parsed.content); + const fallbackName = parsed.path ? path.basename(parsed.path, path.extname(parsed.path)) : null; + const name = frontmatter + ? extractFrontmatterField(frontmatter, 'name') || fallbackName + : fallbackName; + if (!name || !KIMI_CANONICAL_GSD_AGENT_RE.test(name)) { + addKimiAgentDiagnostic( + diagnostics, + 'kimi_invalid_subagent_name', + 'Subagent source does not use a canonical gsd-* Kimi agent name.', + name || '(missing)', + parsed.path + ); + continue; + } + + const description = frontmatter + ? extractFrontmatterField(frontmatter, 'description') || `Run ${name}.` + : `Run ${name}.`; + const tools = mapKimiAgentContractTools(parseFrontmatterTools(frontmatter), diagnostics, name); + subagentMap.set(name, { + name, + description, + tools, + prompt: neutralizeKimiAgentPrompt(parsed.content), + }); + } + + const requested = Array.isArray(requestedSubagents) && requestedSubagents.length > 0 + ? requestedSubagents + : [...subagentMap.keys()]; + const selectedSubagents = []; + for (const requestedName of requested) { + if (subagentMap.has(requestedName)) { + selectedSubagents.push(subagentMap.get(requestedName)); + continue; + } + addKimiAgentDiagnostic( + diagnostics, + 'kimi_unknown_subagent', + 'Requested subagent was not generated and will not be emitted in Kimi YAML.', + requestedName, + null + ); + } + + const rootTools = mapKimiAgentContractTools(parseFrontmatterTools(rootFrontmatter), diagnostics, 'gsd'); + if (selectedSubagents.length > 0 && !rootTools.includes('kimi_cli.tools.agent:Agent')) { + rootTools.push('kimi_cli.tools.agent:Agent'); + } + + return { + root: { + name: 'gsd', + yamlPath: 'agents/gsd.yaml', + promptPath: 'agents/gsd.md', + yaml: buildKimiRootAgentYaml({ + description: rootDescription, + tools: rootTools, + subagents: selectedSubagents, + }), + prompt: neutralizeKimiAgentPrompt(rootSource.content), + }, + subagents: selectedSubagents.map((subagent) => ({ + name: subagent.name, + yamlPath: `agents/subagents/${subagent.name}.yaml`, + promptPath: `agents/subagents/${subagent.name}.md`, + yaml: buildKimiSubagentYaml(subagent), + prompt: subagent.prompt, + })), + diagnostics, + }; +} + /** * Convert a Claude agent (.md) to a Copilot agent (.agent.md). * Applies tool mapping + deduplication, formats tools as JSON array. @@ -11041,6 +11319,7 @@ module.exports = { uninstall, convertClaudeCommandToCodexSkill, convertClaudeCommandToKimiSkill, + buildKimiAgentArtifacts, convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, configureOpencodePermissions, From 260f276287a1da0d4b715d024a4497f9b69a9ae7 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:42:57 +0200 Subject: [PATCH 12/30] test(03-03): add failing Kimi tool mapper tests - cover Kimi module path mappings - assert stable dedupe and exclusion diagnostics --- tests/kimi-tool-mapping.test.cjs | 104 +++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/kimi-tool-mapping.test.cjs diff --git a/tests/kimi-tool-mapping.test.cjs b/tests/kimi-tool-mapping.test.cjs new file mode 100644 index 000000000..324dfc4d3 --- /dev/null +++ b/tests/kimi-tool-mapping.test.cjs @@ -0,0 +1,104 @@ +/** + * Kimi CLI tool module mapper tests. + * + * Kimi custom agent YAML requires tool module paths such as + * `kimi_cli.tools.file:ReadFile`; raw Claude tool names and MCP-managed tools + * must not be emitted. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + convertKimiToolName, + mapClaudeToolsToKimiTools, +} = require('../bin/install.js'); + +describe('convertKimiToolName', () => { + test('maps Claude and GSD tool names to documented Kimi module paths', () => { + const expectedMappings = new Map([ + ['Read', 'kimi_cli.tools.file:ReadFile'], + ['Write', 'kimi_cli.tools.file:WriteFile'], + ['Edit', 'kimi_cli.tools.file:StrReplaceFile'], + ['MultiEdit', 'kimi_cli.tools.file:StrReplaceFile'], + ['Bash', 'kimi_cli.tools.shell:Shell'], + ['Grep', 'kimi_cli.tools.file:Grep'], + ['Glob', 'kimi_cli.tools.file:Glob'], + ['Agent', 'kimi_cli.tools.agent:Agent'], + ['Task', 'kimi_cli.tools.agent:Agent'], + ['AskUserQuestion', 'kimi_cli.tools.ask_user:AskUserQuestion'], + ['TodoWrite', 'kimi_cli.tools.todo:SetTodoList'], + ['WebSearch', 'kimi_cli.tools.web:SearchWeb'], + ['WebFetch', 'kimi_cli.tools.web:FetchURL'], + ]); + + for (const [claudeTool, kimiTool] of expectedMappings) { + assert.equal(convertKimiToolName(claudeTool), kimiTool, `${claudeTool} maps to Kimi module path`); + } + }); + + test('returns null for MCP-managed and unsupported tools', () => { + assert.equal(convertKimiToolName('mcp__context7__resolve-library-id'), null); + assert.equal(convertKimiToolName('DefinitelyUnknownTool'), null); + }); +}); + +describe('mapClaudeToolsToKimiTools', () => { + test('deduplicates mapped tools while preserving first-seen order', () => { + const result = mapClaudeToolsToKimiTools([ + 'Read', + 'ReadFile', + 'Edit', + 'MultiEdit', + 'Bash', + 'Task', + 'Agent', + 'WebFetch', + ]); + + assert.deepEqual(result.tools, [ + 'kimi_cli.tools.file:ReadFile', + 'kimi_cli.tools.file:StrReplaceFile', + 'kimi_cli.tools.shell:Shell', + 'kimi_cli.tools.agent:Agent', + 'kimi_cli.tools.web:FetchURL', + ]); + assert.deepEqual(result.diagnostics, []); + }); + + test('excludes MCP-managed tools with diagnostics', () => { + const result = mapClaudeToolsToKimiTools([ + 'Read', + 'mcp__context7__resolve-library-id', + ]); + + assert.deepEqual(result.tools, ['kimi_cli.tools.file:ReadFile']); + assert.ok( + result.diagnostics.some((item) => + item.reason === 'mcp_managed' && + item.code === 'kimi_mcp_tool_excluded' && + item.value === 'mcp__context7__resolve-library-id' + ), + 'MCP-managed exclusion is diagnosed' + ); + }); + + test('excludes unsupported tools with diagnostics', () => { + const result = mapClaudeToolsToKimiTools([ + 'Read', + 'DefinitelyUnknownTool', + ]); + + assert.deepEqual(result.tools, ['kimi_cli.tools.file:ReadFile']); + assert.ok( + result.diagnostics.some((item) => + item.reason === 'unsupported_tool' && + item.code === 'kimi_unsupported_tool' && + item.value === 'DefinitelyUnknownTool' + ), + 'unsupported tool exclusion is diagnosed' + ); + }); +}); From 8508ad35d8f7326d0649df5369a1effa99493956 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:44:26 +0200 Subject: [PATCH 13/30] feat(03-03): implement Kimi tool module mapper - add exported Claude-to-Kimi tool conversion helpers - route Kimi agent artifact tools through mapper diagnostics --- bin/install.js | 132 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 41 deletions(-) diff --git a/bin/install.js b/bin/install.js index 0ea212dc9..121f47b57 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1868,6 +1868,35 @@ const claudeToGeminiTools = { TodoWrite: 'write_todos', }; +// Tool name mapping from Claude/GSD agents to Kimi CLI module paths. +// Kimi custom agent YAML requires fully-qualified module paths. +const claudeToKimiTools = { + Read: 'kimi_cli.tools.file:ReadFile', + ReadFile: 'kimi_cli.tools.file:ReadFile', + Write: 'kimi_cli.tools.file:WriteFile', + WriteFile: 'kimi_cli.tools.file:WriteFile', + Edit: 'kimi_cli.tools.file:StrReplaceFile', + MultiEdit: 'kimi_cli.tools.file:StrReplaceFile', + StrReplaceFile: 'kimi_cli.tools.file:StrReplaceFile', + Bash: 'kimi_cli.tools.shell:Shell', + Shell: 'kimi_cli.tools.shell:Shell', + Grep: 'kimi_cli.tools.file:Grep', + Glob: 'kimi_cli.tools.file:Glob', + Agent: 'kimi_cli.tools.agent:Agent', + Task: 'kimi_cli.tools.agent:Agent', + AskUserQuestion: 'kimi_cli.tools.ask_user:AskUserQuestion', + TodoWrite: 'kimi_cli.tools.todo:SetTodoList', + SetTodoList: 'kimi_cli.tools.todo:SetTodoList', + WebSearch: 'kimi_cli.tools.web:SearchWeb', + SearchWeb: 'kimi_cli.tools.web:SearchWeb', + WebFetch: 'kimi_cli.tools.web:FetchURL', + FetchURL: 'kimi_cli.tools.web:FetchURL', + ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile', + TaskList: 'kimi_cli.tools.task:TaskList', + TaskOutput: 'kimi_cli.tools.task:TaskOutput', + TaskStop: 'kimi_cli.tools.task:TaskStop', +}; + /** * Convert a Claude Code tool name to OpenCode format * - Applies special mappings (AskUserQuestion -> question, etc.) @@ -1917,6 +1946,63 @@ function convertGeminiToolName(claudeTool) { return claudeTool.toLowerCase(); } +function createKimiToolDiagnostic(reason, tool, source = null) { + const isMcp = reason === 'mcp_managed'; + return { + level: 'warning', + code: isMcp ? 'kimi_mcp_tool_excluded' : 'kimi_unsupported_tool', + reason, + message: isMcp + ? `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.` + : `Tool '${tool}' is not supported by the Kimi tool mapper.`, + value: tool, + source, + }; +} + +/** + * Convert a Claude/GSD tool name to a Kimi CLI module path. + * @returns {string|null} Kimi module path, or null when excluded/unsupported. + */ +function convertKimiToolName(claudeTool) { + const tool = String(claudeTool || '').trim(); + if (!tool) return null; + if (tool.startsWith('mcp__')) return null; + return claudeToKimiTools[tool] || null; +} + +function mapClaudeToolsToKimiTools(claudeTools, options = {}) { + const diagnostics = []; + const tools = []; + const seen = new Set(); + const source = options && Object.prototype.hasOwnProperty.call(options, 'source') + ? options.source + : null; + + for (const rawTool of Array.isArray(claudeTools) ? claudeTools : []) { + const tool = String(rawTool || '').trim(); + if (!tool) continue; + + if (tool.startsWith('mcp__')) { + diagnostics.push(createKimiToolDiagnostic('mcp_managed', tool, source)); + continue; + } + + const kimiTool = convertKimiToolName(tool); + if (!kimiTool) { + diagnostics.push(createKimiToolDiagnostic('unsupported_tool', tool, source)); + continue; + } + + if (!seen.has(kimiTool)) { + seen.add(kimiTool); + tools.push(kimiTool); + } + } + + return { tools, diagnostics }; +} + const claudeToKiloAgentPermissions = { Read: 'read', Write: 'edit', @@ -2183,10 +2269,6 @@ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cm } const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/; -const kimiAgentContractToolMap = { - Agent: 'kimi_cli.tools.agent:Agent', - Task: 'kimi_cli.tools.agent:Agent', -}; function parseKimiAgentSource(source) { if (typeof source === 'string') { @@ -2254,43 +2336,9 @@ function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null } function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) { - const mapped = []; - const seen = new Set(); - - for (const rawTool of toolNames) { - const tool = String(rawTool || '').trim(); - if (!tool) continue; - - if (tool.startsWith('mcp__')) { - addKimiAgentDiagnostic( - diagnostics, - 'kimi_mcp_tool_excluded', - `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.`, - tool, - sourceName - ); - continue; - } - - const kimiTool = kimiAgentContractToolMap[tool]; - if (!kimiTool) { - addKimiAgentDiagnostic( - diagnostics, - 'kimi_unsupported_tool', - `Tool '${tool}' is not part of the Phase 3 Kimi agent contract mapper.`, - tool, - sourceName - ); - continue; - } - - if (!seen.has(kimiTool)) { - seen.add(kimiTool); - mapped.push(kimiTool); - } - } - - return mapped; + const result = mapClaudeToolsToKimiTools(toolNames, { source: sourceName }); + diagnostics.push(...result.diagnostics); + return result.tools; } function neutralizeKimiAgentPrompt(content) { @@ -11319,6 +11367,8 @@ module.exports = { uninstall, convertClaudeCommandToCodexSkill, convertClaudeCommandToKimiSkill, + convertKimiToolName, + mapClaudeToolsToKimiTools, buildKimiAgentArtifacts, convertClaudeToOpencodeFrontmatter, convertClaudeToKiloFrontmatter, From 16553f3c36742c6e0d81b2a2d5ea82e384fe784c Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:45:27 +0200 Subject: [PATCH 14/30] test(03-03): assert Kimi agent tool mapper wiring - verify generated YAML emits Kimi module paths - assert raw Claude and MCP tool names stay excluded --- tests/kimi-agent-converter.test.cjs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/kimi-agent-converter.test.cjs b/tests/kimi-agent-converter.test.cjs index e7cdb5bda..46bb4751c 100644 --- a/tests/kimi-agent-converter.test.cjs +++ b/tests/kimi-agent-converter.test.cjs @@ -65,6 +65,11 @@ describe('buildKimiAgentArtifacts', () => { assert.ok(result.root.yaml.includes('system_prompt_path: ./gsd.md'), 'root prompt path is relative'); assert.ok(result.root.yaml.includes('tools:'), 'root YAML has a tools field'); assert.ok(result.root.yaml.includes('kimi_cli.tools.agent:Agent'), 'root can call Kimi subagents'); + assert.ok( + result.root.yaml.includes('kimi_cli.tools.agent:Agent'), + 'root tools use Kimi module paths' + ); + assert.ok(!result.root.yaml.includes('- Agent'), 'root YAML does not emit raw Claude Agent tool'); assert.ok(result.root.yaml.includes('subagents:'), 'root YAML declares custom subagents'); assert.ok(result.root.yaml.includes('gsd-executor:'), 'known GSD subagent key is canonical'); assert.ok( @@ -89,6 +94,15 @@ describe('buildKimiAgentArtifacts', () => { assert.equal(executor.yamlPath, 'agents/subagents/gsd-executor.yaml'); assert.equal(executor.promptPath, 'agents/subagents/gsd-executor.md'); assert.ok(executor.yaml.includes('system_prompt_path: ./gsd-executor.md')); + assert.ok(executor.yaml.includes('kimi_cli.tools.file:ReadFile'), 'Read maps to Kimi file tool'); + assert.ok(executor.yaml.includes('kimi_cli.tools.file:WriteFile'), 'Write maps to Kimi file tool'); + assert.ok(executor.yaml.includes('kimi_cli.tools.file:StrReplaceFile'), 'Edit maps to Kimi file tool'); + assert.ok(executor.yaml.includes('kimi_cli.tools.shell:Shell'), 'Bash maps to Kimi shell tool'); + assert.ok(executor.yaml.includes('kimi_cli.tools.file:Grep'), 'Grep maps to Kimi grep tool'); + assert.ok(executor.yaml.includes('kimi_cli.tools.file:Glob'), 'Glob maps to Kimi glob tool'); + assert.ok(!executor.yaml.includes('- Read'), 'subagent YAML does not emit raw Claude Read tool'); + assert.ok(!executor.yaml.includes('- Bash'), 'subagent YAML does not emit raw Claude Bash tool'); + assert.ok(!executor.yaml.includes('kimi_cli.tools.agent:Agent'), 'subagent does not inherit nested Agent tool'); for (const prompt of [result.root.prompt, executor.prompt]) { assert.ok(!prompt.trimStart().startsWith('---'), 'source frontmatter is removed'); @@ -127,10 +141,18 @@ describe('buildKimiAgentArtifacts', () => { result.diagnostics.some((item) => item.code === 'kimi_mcp_tool_excluded'), 'MCP-managed tools are diagnosed and excluded' ); + assert.ok( + result.diagnostics.some((item) => item.reason === 'mcp_managed'), + 'MCP diagnostics expose mapper reason' + ); assert.ok( result.diagnostics.some((item) => item.code === 'kimi_unsupported_tool'), 'unsupported tools are diagnosed and excluded' ); + assert.ok( + result.diagnostics.some((item) => item.reason === 'unsupported_tool'), + 'unsupported diagnostics expose mapper reason' + ); assert.ok(!result.root.yaml.includes('mcp__github__search'), 'MCP tool names are not emitted'); assert.ok(!result.root.yaml.includes('DefinitelyUnknownTool'), 'unsupported tool names are not emitted'); }); From de5c57c68502bec542b91fb5280bfcd230492bcf Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:55:38 +0200 Subject: [PATCH 15/30] test(03-02): add failing Kimi agent install tests - assert Kimi global layout stages agent YAML and prompts - expect global install to write agents/gsd.yaml while local guard stays no-op --- .../bug-kimi-path-layout-local-guard.test.cjs | 37 +++++++++++++++-- tests/install-runtime-artifacts.test.cjs | 40 +++++++++++++++++++ tests/runtime-artifact-layout.test.cjs | 36 ++++++++++++++++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 2846e82b7..40483a4d9 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -94,15 +94,19 @@ describe('Kimi runtime homes', () => { }); describe('Kimi runtime artifact layout', () => { - test('global layout stages Kimi skills while local layout remains guarded', () => { + test('global layout stages Kimi skills and agents while local layout remains guarded', () => { const globalLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'global'); assert.strictEqual(globalLayout.runtime, 'kimi'); assert.strictEqual(globalLayout.configDir, '/tmp/kimi-config'); - assert.strictEqual(globalLayout.kinds.length, 1); + assert.strictEqual(globalLayout.kinds.length, 2); assert.strictEqual(globalLayout.kinds[0].kind, 'skills'); assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills'); assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-'); assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function'); + assert.strictEqual(globalLayout.kinds[1].kind, 'kimi-agents'); + assert.strictEqual(globalLayout.kinds[1].destSubpath, 'agents'); + assert.strictEqual(globalLayout.kinds[1].prefix, 'gsd'); + assert.strictEqual(typeof globalLayout.kinds[1].stage, 'function'); const localLayout = resolveRuntimeArtifactLayout('kimi', '/tmp/kimi-config', 'local'); assert.strictEqual(localLayout.runtime, 'kimi'); @@ -143,7 +147,7 @@ describe('Kimi local install guard', () => { } }); - test('--kimi --global writes converted Kimi skills only', () => { + test('--kimi --global writes converted Kimi skills and agent artifacts', () => { const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-project-')); const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-config-')); const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-global-home-')); @@ -166,6 +170,8 @@ describe('Kimi local install guard', () => { const combined = `${result.stdout}\n${result.stderr}`; assert.match(combined, /Installing for .*Kimi/i); assert.match(combined, /Installed \d+ skills to skills\//i); + assert.match(combined, /Generated Kimi root agent: .*agents.*gsd\.yaml/i); + assert.match(combined, /kimi --agent-file/i); const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); assert.ok(fs.existsSync(skillFile), 'must write gsd-new-project/SKILL.md'); @@ -174,7 +180,30 @@ describe('Kimi local install guard', () => { assert.match(skillContent, /\/skill:gsd-new-project/); assert.doesNotMatch(skillContent, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); - assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents')), 'must not write Kimi agents'); + const rootYaml = path.join(tmpConfig, 'agents', 'gsd.yaml'); + const rootPrompt = path.join(tmpConfig, 'agents', 'gsd.md'); + const executorYaml = path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.yaml'); + const executorPrompt = path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.md'); + assert.ok(fs.existsSync(rootYaml), 'must write agents/gsd.yaml'); + assert.ok(fs.existsSync(rootPrompt), 'must write agents/gsd.md'); + assert.ok(fs.existsSync(executorYaml), 'must write agents/subagents/gsd-executor.yaml'); + assert.ok(fs.existsSync(executorPrompt), 'must write agents/subagents/gsd-executor.md'); + + const rootYamlContent = fs.readFileSync(rootYaml, 'utf8'); + assert.match(rootYamlContent, /^version: 1$/m); + assert.match(rootYamlContent, /^agent:$/m); + assert.match(rootYamlContent, /extend: default/); + assert.match(rootYamlContent, /system_prompt_path: \.\/gsd\.md/); + assert.match(rootYamlContent, /tools:/); + assert.match(rootYamlContent, /subagents:/); + assert.match(rootYamlContent, /kimi_cli\.tools\./); + assert.doesNotMatch(rootYamlContent, /mcp__/); + + const executorYamlContent = fs.readFileSync(executorYaml, 'utf8'); + assert.match(executorYamlContent, /system_prompt_path: \.\/gsd-executor\.md/); + assert.match(executorYamlContent, /kimi_cli\.tools\./); + assert.doesNotMatch(executorYamlContent, /mcp__/); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts'); assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root'); } finally { diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index e5e867f7d..c224c336b 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -101,6 +101,31 @@ describe('installRuntimeArtifacts — skills runtimes write gsd-prefixed skill d assert.match(content, /^name: gsd-new-project$/m); assert.match(content, /\/skill:gsd-new-project/); assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); + + const agentsDir = path.join(configDir, 'agents'); + const rootYaml = path.join(agentsDir, 'gsd.yaml'); + const rootPrompt = path.join(agentsDir, 'gsd.md'); + const executorYaml = path.join(agentsDir, 'subagents', 'gsd-executor.yaml'); + const executorPrompt = path.join(agentsDir, 'subagents', 'gsd-executor.md'); + assert.ok(fs.existsSync(rootYaml), 'kimi: agents/gsd.yaml must exist'); + assert.ok(fs.existsSync(rootPrompt), 'kimi: agents/gsd.md must exist'); + assert.ok(fs.existsSync(executorYaml), 'kimi: agents/subagents/gsd-executor.yaml must exist'); + assert.ok(fs.existsSync(executorPrompt), 'kimi: agents/subagents/gsd-executor.md must exist'); + + const rootYamlContent = fs.readFileSync(rootYaml, 'utf8'); + assert.match(rootYamlContent, /^version: 1$/m); + assert.match(rootYamlContent, /^agent:$/m); + assert.match(rootYamlContent, /extend: default/); + assert.match(rootYamlContent, /system_prompt_path: \.\/gsd\.md/); + assert.match(rootYamlContent, /tools:/); + assert.match(rootYamlContent, /subagents:/); + assert.match(rootYamlContent, /kimi_cli\.tools\./); + assert.doesNotMatch(rootYamlContent, /mcp__/); + + const executorYamlContent = fs.readFileSync(executorYaml, 'utf8'); + assert.match(executorYamlContent, /system_prompt_path: \.\/gsd-executor\.md/); + assert.match(executorYamlContent, /kimi_cli\.tools\./); + assert.doesNotMatch(executorYamlContent, /mcp__/); } if (RESOLVED_CORE.skills !== '*') { @@ -213,6 +238,14 @@ describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves for const foreignDir = path.join(destDir, 'user-custom-skill'); fs.mkdirSync(foreignDir, { recursive: true }); fs.writeFileSync(path.join(foreignDir, 'SKILL.md'), '# user\n'); + } else if (kind.kind === 'kimi-agents') { + fs.mkdirSync(path.join(destDir, 'subagents'), { recursive: true }); + fs.writeFileSync(path.join(destDir, 'gsd.yaml'), 'version: 1\n'); + fs.writeFileSync(path.join(destDir, 'gsd.md'), '# gsd\n'); + fs.writeFileSync(path.join(destDir, 'subagents', 'gsd-executor.yaml'), 'version: 1\n'); + fs.writeFileSync(path.join(destDir, 'subagents', 'gsd-executor.md'), '# executor\n'); + fs.writeFileSync(path.join(destDir, 'user-agent.yaml'), 'version: 1\n'); + fs.writeFileSync(path.join(destDir, 'subagents', 'user-agent.yaml'), 'version: 1\n'); } else { writeCommandEntry(destDir, kind.prefix, 'help'); writeCommandEntry(destDir, kind.prefix, 'phase'); @@ -228,6 +261,13 @@ describe('uninstallRuntimeArtifacts — removes gsd-owned entries, preserves for assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}help`))); assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}phase`))); assert.ok(fs.existsSync(path.join(destDir, 'user-custom-skill', 'SKILL.md'))); + } else if (kind.kind === 'kimi-agents') { + assert.ok(!fs.existsSync(path.join(destDir, 'gsd.yaml'))); + assert.ok(!fs.existsSync(path.join(destDir, 'gsd.md'))); + assert.ok(!fs.existsSync(path.join(destDir, 'subagents', 'gsd-executor.yaml'))); + assert.ok(!fs.existsSync(path.join(destDir, 'subagents', 'gsd-executor.md'))); + assert.ok(fs.existsSync(path.join(destDir, 'user-agent.yaml'))); + assert.ok(fs.existsSync(path.join(destDir, 'subagents', 'user-agent.yaml'))); } else { assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}help.md`))); assert.ok(!fs.existsSync(path.join(destDir, `${kind.prefix}phase.md`))); diff --git a/tests/runtime-artifact-layout.test.cjs b/tests/runtime-artifact-layout.test.cjs index 415b70c3e..bb1d5d541 100644 --- a/tests/runtime-artifact-layout.test.cjs +++ b/tests/runtime-artifact-layout.test.cjs @@ -180,11 +180,15 @@ describe('resolveRuntimeArtifactLayout — kimi', () => { const globalLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'global'); assert.strictEqual(globalLayout.runtime, 'kimi'); assert.strictEqual(globalLayout.configDir, FAKE_DIR); - assert.strictEqual(globalLayout.kinds.length, 1); + assert.strictEqual(globalLayout.kinds.length, 2); assert.strictEqual(globalLayout.kinds[0].kind, 'skills'); assert.strictEqual(globalLayout.kinds[0].destSubpath, 'skills'); assert.strictEqual(globalLayout.kinds[0].prefix, 'gsd-'); assert.strictEqual(typeof globalLayout.kinds[0].stage, 'function'); + assert.strictEqual(globalLayout.kinds[1].kind, 'kimi-agents'); + assert.strictEqual(globalLayout.kinds[1].destSubpath, 'agents'); + assert.strictEqual(globalLayout.kinds[1].prefix, 'gsd'); + assert.strictEqual(typeof globalLayout.kinds[1].stage, 'function'); const localLayout = resolveRuntimeArtifactLayout('kimi', FAKE_DIR, 'local'); assert.strictEqual(localLayout.runtime, 'kimi'); @@ -398,7 +402,7 @@ describe('stage — skills kind (claude global)', () => { }); describe('stage — skills kind (kimi global)', () => { - test('stage returns Kimi SKILL.md dirs with /skill:gsd-* invocations', () => { + test('stage returns Kimi SKILL.md dirs and agent YAML/prompt artifacts', () => { const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global'); const skillsKind = layout.kinds.find(k => k.kind === 'skills'); assert.ok(skillsKind, 'should have a skills kind'); @@ -411,6 +415,34 @@ describe('stage — skills kind (kimi global)', () => { assert.match(content, /^name: gsd-new-project$/m); assert.match(content, /\/skill:gsd-new-project/); assert.doesNotMatch(content, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); + + const agentsKind = layout.kinds.find(k => k.kind === 'kimi-agents'); + assert.ok(agentsKind, 'should have a kimi-agents kind'); + + const stagedAgentsDir = agentsKind.stage(PROFILE_FULL); + const rootYamlPath = path.join(stagedAgentsDir, 'gsd.yaml'); + const rootPromptPath = path.join(stagedAgentsDir, 'gsd.md'); + const executorYamlPath = path.join(stagedAgentsDir, 'subagents', 'gsd-executor.yaml'); + const executorPromptPath = path.join(stagedAgentsDir, 'subagents', 'gsd-executor.md'); + assert.ok(fs.existsSync(rootYamlPath), 'agents/gsd.yaml must be staged'); + assert.ok(fs.existsSync(rootPromptPath), 'agents/gsd.md must be staged'); + assert.ok(fs.existsSync(executorYamlPath), 'agents/subagents/gsd-executor.yaml must be staged'); + assert.ok(fs.existsSync(executorPromptPath), 'agents/subagents/gsd-executor.md must be staged'); + + const rootYaml = fs.readFileSync(rootYamlPath, 'utf8'); + assert.match(rootYaml, /^version: 1$/m); + assert.match(rootYaml, /^agent:$/m); + assert.match(rootYaml, /extend: default/); + assert.match(rootYaml, /system_prompt_path: \.\/gsd\.md/); + assert.match(rootYaml, /tools:/); + assert.match(rootYaml, /subagents:/); + assert.match(rootYaml, /kimi_cli\.tools\./); + assert.doesNotMatch(rootYaml, /mcp__/); + + const executorYaml = fs.readFileSync(executorYamlPath, 'utf8'); + assert.match(executorYaml, /system_prompt_path: \.\/gsd-executor\.md/); + assert.match(executorYaml, /kimi_cli\.tools\./); + assert.doesNotMatch(executorYaml, /mcp__/); }); }); From fba6cfe63d491e3f9a06c0f70ae6145936b77a9c Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:57:48 +0200 Subject: [PATCH 16/30] feat(03-02): install Kimi global agent artifacts - stage generated Kimi root and subagent YAML/prompt files under agents/ - copy and remove only GSD-owned Kimi agent files while preserving user files - print explicit kimi --agent-file launch hint --- bin/install.js | 31 +++++++++++++++++++- src/runtime-artifact-layout.cts | 52 +++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index 121f47b57..1d89abdd0 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6686,6 +6686,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { * encodes the GSD namespace as its last segment (e.g. `commands/gsd`), in * which case write as `${stem}.md` (directory IS the namespace). * - agents: write as-is (files already carry their own `gsd-` prefix). + * For kimi-agents kind: recursively copy generated YAML/prompt files. */ function _copyStaged(stagedDir, destDir, kind) { if (!fs.existsSync(stagedDir)) return; @@ -6702,6 +6703,11 @@ function _copyStaged(stagedDir, destDir, kind) { return; } + if (kind.kind === 'kimi-agents') { + fs.cpSync(stagedDir, destDir, { recursive: true }); + return; + } + // commands or agents const entries = fs.readdirSync(stagedDir, { withFileTypes: true }); // For commands: apply prefix unless the destSubpath's last segment already @@ -6738,6 +6744,21 @@ function _copyStaged(stagedDir, destDir, kind) { */ function _removeGsdEntries(destDir, kind) { if (!fs.existsSync(destDir)) return; + if (kind.kind === 'kimi-agents') { + for (const fileName of ['gsd.yaml', 'gsd.md']) { + fs.rmSync(path.join(destDir, fileName), { force: true }); + } + const subagentsDir = path.join(destDir, 'subagents'); + if (fs.existsSync(subagentsDir)) { + for (const entry of fs.readdirSync(subagentsDir, { withFileTypes: true })) { + if (!entry.isFile()) continue; + if (!entry.name.startsWith('gsd-')) continue; + if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue; + fs.rmSync(path.join(subagentsDir, entry.name), { force: true }); + } + } + return; + } if (kind.prefix === '') { // Whole-namespace removal (Hermes nested case — destSubpath is skills/gsd) // The directory itself is the GSD namespace, so remove it entirely. @@ -8779,6 +8800,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { if (isKimi && isGlobal) { installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); const skillsDir = path.join(targetDir, 'skills'); + const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); const count = fs.existsSync(skillsDir) ? fs.readdirSync(skillsDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length @@ -8788,11 +8810,18 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { throw new Error('Kimi global install produced no skills/gsd-* entries'); } + if (fs.existsSync(rootAgentPath)) { + console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); + console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); + } else { + throw new Error('Kimi global install produced no agents/gsd.yaml'); + } return { runtime, skipped: true, - reason: 'kimi_global_skills_only', + reason: 'kimi_global_skills_and_agents', configDir: targetDir, + agentPath: rootAgentPath, settingsPath: null, settings: null, statuslineCommand: null, diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index dfe874a87..b68fb2a9c 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -64,6 +64,7 @@ function getInstallExports(): InstallExports { // --------------------------------------------------------------------------- type ArtifactKindName = 'commands' | 'agents' | 'skills'; +type KimiArtifactKindName = ArtifactKindName | 'kimi-agents'; // Mirrors the (unexported) ResolvedProfile in install-profiles.cts. // Must stay in sync if that shape changes. @@ -74,7 +75,7 @@ interface ResolvedProfile { } interface ArtifactKind { - kind: ArtifactKindName; + kind: KimiArtifactKindName; destSubpath: string; prefix: string; stage: (resolvedProfile: ResolvedProfile) => string; @@ -192,6 +193,50 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art }; } +function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind { + return { + kind: 'kimi-agents', + destSubpath, + prefix, + stage: (resolved) => { + const installExports = getInstallExports(); + const buildKimiAgentArtifacts = installExports['buildKimiAgentArtifacts'] as (opts: { + rootAgent?: string; + subagents?: Array<{ path: string; content: string }>; + }) => { + root: { yaml: string; prompt: string }; + subagents: Array<{ name: string; yaml: string; prompt: string }>; + }; + const stagedAgents = stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved); + const subagents: Array<{ path: string; content: string }> = []; + if (fs.existsSync(stagedAgents)) { + for (const entry of fs.readdirSync(stagedAgents, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.md')) continue; + const agentPath = path.join(stagedAgents, entry.name); + subagents.push({ + path: path.join('agents', entry.name).replace(/\\/g, '/'), + content: fs.readFileSync(agentPath, 'utf8'), + }); + } + } + + const rootAgent = `---\nname: gsd\ndescription: Run GSD workflows in Kimi CLI.\ntools: Agent\n---\n\n# GSD for Kimi CLI\n\nCoordinate installed /skill:gsd-* workflows and route work to generated GSD subagents when a workflow requires an agent handoff.\n`; + const artifacts = buildKimiAgentArtifacts({ rootAgent, subagents }); + const stageDir = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'gsd-kimi-agents-')); + fs.writeFileSync(path.join(stageDir, 'gsd.yaml'), artifacts.root.yaml); + fs.writeFileSync(path.join(stageDir, 'gsd.md'), artifacts.root.prompt); + const subagentsDir = path.join(stageDir, 'subagents'); + fs.mkdirSync(subagentsDir, { recursive: true }); + for (const artifact of artifacts.subagents) { + fs.writeFileSync(path.join(subagentsDir, `${artifact.name}.yaml`), artifact.yaml); + fs.writeFileSync(path.join(subagentsDir, `${artifact.name}.md`), artifact.prompt); + } + installProfiles.STAGED_DIRS.add(stageDir); + return stageDir; + }, + }; +} + /** * Build a skills kind descriptor. * @@ -306,7 +351,10 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: case 'kimi': kinds = scope === 'global' - ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir)] + ? [ + skillsKind('skills', 'gsd-', 'convertClaudeCommandToKimiSkill', 'kimi', configDir), + kimiAgentsKind('agents', 'gsd', configDir), + ] : []; break; From 13e2682d280078dc113e01176ef87bb2c1d0769f Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:13:42 +0200 Subject: [PATCH 17/30] docs(04-01): document Kimi install surfaces - add Kimi CLI to README runtime support lists - document Kimi global install, skill invocation, and explicit agent-file launch --- README.md | 6 +++--- docs/how-to/install-on-your-runtime.md | 30 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c111e412e..db7187704 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **English** · [Português](README.pt-BR.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [한국어](README.ko-KR.md) -**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.** +**A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Gemini CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.** [![npm version](https://img.shields.io/npm/v/%40opengsd%2Fgsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@opengsd/gsd-core) [![npm downloads](https://img.shields.io/npm/dm/%40opengsd%2Fgsd-core?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/@opengsd/gsd-core) @@ -21,7 +21,7 @@ ## What is GSD Core -GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Gemini CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. +GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Gemini CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves [context rot](docs/explanation/context-engineering.md) — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean. --- @@ -43,7 +43,7 @@ Each milestone repeats the same five-step loop, one phase at a time: npx @opengsd/gsd-core@latest ``` -The installer prompts for your runtime (Claude Code, OpenCode, Gemini CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. +The installer prompts for your runtime (Claude Code, OpenCode, Gemini CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from `agents/` or `commands/` directly. On another runtime or without Node.js? See [Install on your runtime](docs/how-to/install-on-your-runtime.md). diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 0e2a160e7..5fad4b8d3 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -104,6 +104,36 @@ Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written with per-age --- +### Kimi CLI + +```bash +npx @opengsd/gsd-core@latest --kimi --global +``` + +Skills land in `~/.config/agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: + +```text +/skill:gsd-new-project +``` + +The installer also writes the GSD custom agent definition to `~/.config/agents/agents/gsd.yaml` with its prompt at `~/.config/agents/agents/gsd.md`; subagents land under `~/.config/agents/agents/subagents/gsd-*.yaml` and `~/.config/agents/agents/subagents/gsd-*.md`. + +Kimi custom agents do not auto-activate just because the files exist. Launch Kimi with the generated agent file when you want the GSD agent surface: + +```bash +kimi --agent-file ~/.config/agents/agents/gsd.yaml +``` + +**Override the install directory:** + +```bash +KIMI_CONFIG_DIR=~/.config/agents-alt npx @opengsd/gsd-core@latest --kimi --global +``` + +`--kimi --local` is intentionally deferred and guarded in v1; use the global install path above for Kimi CLI. + +--- + ### GitHub Copilot ```bash From fdbfd7cb35b08f013e633f19094a7a8760b76e46 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:14:35 +0200 Subject: [PATCH 18/30] docs(04-01): add Kimi runtime architecture notes - add Kimi CLI to runtime contract and install surfaces - document explicit agent-file launch and tool module mapping --- docs/ARCHITECTURE.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0443991e5..48b38563b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -21,7 +21,7 @@ ## System Overview -GSD Core is a **meta-prompting framework** that sits between the user and AI coding agents (Claude Code, Gemini CLI, OpenCode, Kilo, Codex, Copilot, Antigravity, Trae, Cline, Augment Code). It provides: +GSD Core is a **meta-prompting framework** that sits between the user and AI coding agents (Claude Code, Gemini CLI, Kimi CLI, OpenCode, Kilo, Codex, Copilot, Antigravity, Trae, Cline, Augment Code). It provides: 1. **Context engineering** — Structured artifacts that give the AI everything it needs per task (see [Context engineering](explanation/context-engineering.md)) 2. **Multi-agent orchestration** — Thin orchestrators that spawn specialized agents with fresh context windows (see [Multi-agent orchestration](explanation/multi-agent-orchestration.md)) @@ -116,6 +116,7 @@ User-facing entry points. Each file contains YAML frontmatter (name, description - **Codex:** Skills (`$gsd-command-name`) - **Copilot:** Slash commands (hyphen form, `/gsd-command-name`) - **Gemini CLI:** Slash commands under the `gsd:` namespace (colon form, `/gsd:command-name`) — Gemini namespaces all custom commands under their plugin id, so the install path rewrites every body-text reference to colon form +- **Kimi CLI:** Agent Skills (`/skill:gsd-command-name`) plus an explicit custom agent launch with `kimi --agent-file` - **Antigravity:** Skills **Total commands:** see [`docs/INVENTORY.md`](INVENTORY.md#commands) for the authoritative count and full roster. @@ -552,6 +553,7 @@ Equivalent paths for other runtimes: - **OpenCode:** `~/.config/opencode/` global or `./.opencode/` local - **Kilo:** `~/.config/kilo/` global or `./.kilo/` local - **Gemini CLI:** `~/.gemini/` global or `./.gemini/` local +- **Kimi CLI:** `~/.config/agents/` global; local install is deferred and guarded - **Codex:** `~/.codex/` global or `./.codex/` local - **Copilot:** `~/.copilot/` global or `./.github/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local @@ -646,7 +648,7 @@ verification. The installer (`bin/install.js`, ~10,700 lines) handles: -1. **Runtime detection** — Interactive prompt or CLI flags (`--claude`, `--opencode`, `--gemini`, `--kilo`, `--codex`, `--copilot`, `--antigravity`, `--cursor`, `--windsurf`, `--augment`, `--trae`, `--qwen`, `--hermes`, `--codebuddy`, `--cline`, `--all`) +1. **Runtime detection** — Interactive prompt or CLI flags (`--claude`, `--opencode`, `--gemini`, `--kimi`, `--kilo`, `--codex`, `--copilot`, `--antigravity`, `--cursor`, `--windsurf`, `--augment`, `--trae`, `--qwen`, `--hermes`, `--codebuddy`, `--cline`, `--all`) 2. **Location selection** — Global (`--global`) or local (`--local`) 3. **File deployment** — Copies commands, skills, workflows, references, templates, agents, and hooks 4. **Runtime adaptation** — Transforms file content per runtime: @@ -654,6 +656,7 @@ The installer (`bin/install.js`, ~10,700 lines) handles: - OpenCode: Converts commands/agents to OpenCode-compatible flat command + subagent format - Kilo: Reuses the OpenCode conversion pipeline with Kilo config paths - Codex: Generates TOML config + skills from commands + - Kimi CLI: Generates Agent Skills under `skills/gsd-*/SKILL.md`, custom agent YAML/prompt files, and explicit `kimi_cli.tools.*` module paths - Copilot: Maps tool names (Read→read, Bash→execute, etc.) - Gemini: Adjusts hook event names (`AfterTool` instead of `PostToolUse`) - Antigravity: Skills-first with Google model equivalents @@ -789,6 +792,7 @@ The migration-specific ownership and source snapshots live in | OpenCode | `~/.config/opencode` | `./.opencode` | `command/gsd-*.md` | `agents/gsd-*.md` | `opencode.json` or `opencode.jsonc`; no GSD hooks | | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Gemini CLI | `~/.gemini` | `./.gemini` | `commands/gsd/*.toml` | `agents/gsd-*.md` | `settings.json` feature flag, hooks, and statusline | +| Kimi CLI | `~/.config/agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.config/agents/agents/gsd.yaml`; no GSD hooks or statusline | | Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` and `copilot-instructions.md` | `.agent.md` files | No GSD hooks or statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | @@ -810,6 +814,9 @@ available. The current source snapshot is 2026-05-11: - OpenCode and Kilo: OpenCode config docs and Kilo custom subagent docs. - Gemini CLI and Qwen Code: command/config docs; Qwen command docs were last updated 2026-05-06. +- Kimi CLI: Agent Skills docs for `~/.config/agents/skills/` discovery and + Agents docs for YAML files, `system_prompt_path`, `kimi_cli.tools.*` module + paths, and explicit `kimi --agent-file` launch. - Codex: OpenAI Codex docs and `config-schema.json`; the installer also carries Codex 0.124.0 compatibility for agent table shape. - Copilot, Cursor, Cline, Augment, Hermes, and CodeBuddy: vendor docs for From 3d74acf6ab58b043a48af49f75db95aed8a2f720 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:15:13 +0200 Subject: [PATCH 19/30] docs(04-01): add Kimi runtime changeset - add Added changeset fragment for Kimi runtime support --- .changeset/kimi-runtime-support.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/kimi-runtime-support.md diff --git a/.changeset/kimi-runtime-support.md b/.changeset/kimi-runtime-support.md new file mode 100644 index 000000000..29f265ac5 --- /dev/null +++ b/.changeset/kimi-runtime-support.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 817 +--- +**Kimi CLI runtime support is now documented and installable** — users can install global GSD Agent Skills with `--kimi --global`, invoke them as `/skill:gsd-*`, and launch the generated custom agent explicitly with `kimi --agent-file`. From ca0370a7b513e99a6f173e5efd73d1a18756d961 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:02:04 +0200 Subject: [PATCH 20/30] fix(04-02): run Kimi install migrations before artifacts - move Kimi global install after migration gate - align migration integration assertions with Kimi skills and agents contract --- bin/install.js | 67 ++++++++-------- ...ler-migration-install-integration.test.cjs | 79 +++++++++++++------ 2 files changed, 88 insertions(+), 58 deletions(-) diff --git a/bin/install.js b/bin/install.js index 1d89abdd0..e67bb5168 100755 --- a/bin/install.js +++ b/bin/install.js @@ -8797,39 +8797,6 @@ function install(isGlobal, runtime = 'claude', options = {}) { rollback(); }; - if (isKimi && isGlobal) { - installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); - const skillsDir = path.join(targetDir, 'skills'); - const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); - const count = fs.existsSync(skillsDir) - ? fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length - : 0; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); - } else { - throw new Error('Kimi global install produced no skills/gsd-* entries'); - } - if (fs.existsSync(rootAgentPath)) { - console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); - console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); - } else { - throw new Error('Kimi global install produced no agents/gsd.yaml'); - } - return { - runtime, - skipped: true, - reason: 'kimi_global_skills_and_agents', - configDir: targetDir, - agentPath: rootAgentPath, - settingsPath: null, - settings: null, - statuslineCommand: null, - updateBannerCommand: null, - rollbackInstallerMigrations, - }; - } - // Save any locally modified GSD files before they get wiped. // The pristine context lets saveLocalPatches populate gsd-pristine/ via // the install transform pipeline, giving the reapply-patches Step 5 @@ -9077,6 +9044,40 @@ function install(isGlobal, runtime = 'claude', options = {}) { reportInstallerMigrationResult(installerMigrationResult); assertInstallerMigrationsUnblocked(installerMigrationResult); + if (isKimi && isGlobal) { + installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); + const skillsDir = path.join(targetDir, 'skills'); + const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); + const count = fs.existsSync(skillsDir) + ? fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length + : 0; + if (count > 0) { + console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); + } else { + throw new Error('Kimi global install produced no skills/gsd-* entries'); + } + if (fs.existsSync(rootAgentPath)) { + console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); + console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); + } else { + throw new Error('Kimi global install produced no agents/gsd.yaml'); + } + console.log(`\n ${green}Done!${reset} Launch Kimi with ${cyan}kimi --agent-file ${rootAgentPath}${reset}.`); + return { + runtime, + skipped: true, + reason: 'kimi_global_skills_and_agents', + configDir: targetDir, + agentPath: rootAgentPath, + settingsPath: null, + settings: null, + statuslineCommand: null, + updateBannerCommand: null, + rollbackInstallerMigrations, + }; + } + // Artifact install dispatcher — routes to layout-driven path for all // skills-based runtimes (both full and minimal/core profiles); keeps // back-compat paths for commands-based runtimes (OpenCode/Kilo/Gemini/ diff --git a/tests/installer-migration-install-integration.test.cjs b/tests/installer-migration-install-integration.test.cjs index 857bc101f..304d416b0 100644 --- a/tests/installer-migration-install-integration.test.cjs +++ b/tests/installer-migration-install-integration.test.cjs @@ -34,6 +34,7 @@ const RUNTIME_INSTALL_CONTRACTS = { cursor: { surface: 'flat-skills', settings: false, packageJson: false }, gemini: { surface: 'commands-gsd', settings: true, packageJson: true }, hermes: { surface: 'hermes-skills', settings: true, packageJson: true }, + kimi: { surface: 'kimi-skills-agents', settings: false, packageJson: false, workflowPayload: false }, kilo: { surface: 'flat-command', settings: false, packageJson: true }, opencode: { surface: 'flat-command', settings: true, packageJson: true }, qwen: { surface: 'flat-skills', settings: true, packageJson: true }, @@ -179,27 +180,35 @@ function assertFreshInstallContract(runtime, targetDir) { const contract = RUNTIME_INSTALL_CONTRACTS[runtime]; assert.ok(contract, `missing runtime install contract for ${runtime}`); - assert.equal( - fs.readFileSync(path.join(targetDir, 'gsd-core', 'VERSION'), 'utf8'), - pkg.version, - `${runtime} should install the package VERSION` - ); - assert.ok( - fs.existsSync(path.join(targetDir, 'gsd-core', 'bin', 'gsd-tools.cjs')), - `${runtime} should install the GSD tool payload` - ); - assert.ok( - fs.existsSync(path.join(targetDir, 'gsd-file-manifest.json')), - `${runtime} should write the install manifest` - ); + if (contract.workflowPayload !== false) { + assert.equal( + fs.readFileSync(path.join(targetDir, 'gsd-core', 'VERSION'), 'utf8'), + pkg.version, + `${runtime} should install the package VERSION` + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'gsd-core', 'bin', 'gsd-tools.cjs')), + `${runtime} should install the GSD tool payload` + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'gsd-file-manifest.json')), + `${runtime} should write the install manifest` + ); - const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'gsd-file-manifest.json'), 'utf8')); - assert.equal(manifest.version, pkg.version, `${runtime} manifest should record the package version`); - assert.equal(manifest.mode, 'full', `${runtime} manifest should record a full install`); - assert.ok( - manifest.files['gsd-core/VERSION'], - `${runtime} manifest should track the installed VERSION file` - ); + const manifest = JSON.parse(fs.readFileSync(path.join(targetDir, 'gsd-file-manifest.json'), 'utf8')); + assert.equal(manifest.version, pkg.version, `${runtime} manifest should record the package version`); + assert.equal(manifest.mode, 'full', `${runtime} manifest should record a full install`); + assert.ok( + manifest.files['gsd-core/VERSION'], + `${runtime} manifest should track the installed VERSION file` + ); + } else { + assert.equal( + fs.existsSync(path.join(targetDir, 'gsd-core')), + false, + `${runtime} should not install the GSD workflow payload` + ); + } if (contract.surface === 'flat-skills') { // Pre-#3562: codex was special-cased to expect zero gsd-* skill dirs @@ -231,6 +240,20 @@ function assertFreshInstallContract(runtime, targetDir) { listDirNames(targetDir, path.join('commands', 'gsd')).length > 0, `${runtime} should install commands/gsd entries` ); + } else if (contract.surface === 'kimi-skills-agents') { + assertHasGsdDirectory(targetDir, 'skills'); + assert.ok( + fs.existsSync(path.join(targetDir, 'agents', 'gsd.yaml')), + 'Kimi should install the root agent YAML' + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'agents', 'gsd.md')), + 'Kimi should install the root agent prompt' + ); + assert.ok( + fs.existsSync(path.join(targetDir, 'agents', 'subagents', 'gsd-executor.yaml')), + 'Kimi should install GSD subagent YAML' + ); } else if (contract.surface === 'clinerules') { assert.match( fs.readFileSync(path.join(targetDir, '.clinerules'), 'utf8'), @@ -239,10 +262,12 @@ function assertFreshInstallContract(runtime, targetDir) { ); } - assert.ok( - listDirNames(targetDir, 'agents').some((name) => name.startsWith('gsd-')), - `${runtime} full install should install agents` - ); + if (contract.surface !== 'kimi-skills-agents') { + assert.ok( + listDirNames(targetDir, 'agents').some((name) => name.startsWith('gsd-')), + `${runtime} full install should install agents` + ); + } assert.equal( fs.existsSync(path.join(targetDir, 'settings.json')), @@ -427,7 +452,11 @@ describe('installer migration install integration', { concurrency: false }, () = assert.match(output, /Installing for /); assert.match(output, /Installer migrations/); assert.match(output, /removed\s+hooks\/statusline\.js/); - assert.match(output, /Installed workflow assets/); + if (runtime === 'kimi') { + assert.match(output, /Generated Kimi root agent/); + } else { + assert.match(output, /Installed workflow assets/); + } assert.match(output, /Done!/); assert.equal(fs.existsSync(path.join(targetDir, 'hooks/statusline.js')), false); From ce77890a892bbdc6839e3f1746cc837ee99ff97b Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:36:32 +0200 Subject: [PATCH 21/30] fix: import os in runtime artifact layout --- src/runtime-artifact-layout.cts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index b68fb2a9c..3bda4d043 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -15,6 +15,7 @@ import path from 'node:path'; import fs from 'node:fs'; +import os from 'node:os'; // eslint-disable-next-line @typescript-eslint/no-require-imports import installProfiles = require('./install-profiles.cjs'); const { @@ -222,7 +223,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): const rootAgent = `---\nname: gsd\ndescription: Run GSD workflows in Kimi CLI.\ntools: Agent\n---\n\n# GSD for Kimi CLI\n\nCoordinate installed /skill:gsd-* workflows and route work to generated GSD subagents when a workflow requires an agent handoff.\n`; const artifacts = buildKimiAgentArtifacts({ rootAgent, subagents }); - const stageDir = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'gsd-kimi-agents-')); + const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-agents-')); fs.writeFileSync(path.join(stageDir, 'gsd.yaml'), artifacts.root.yaml); fs.writeFileSync(path.join(stageDir, 'gsd.md'), artifacts.root.prompt); const subagentsDir = path.join(stageDir, 'subagents'); From 9f84d50a0dcbb20985b3390984066227c14d7eeb Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sat, 6 Jun 2026 22:15:20 +0200 Subject: [PATCH 22/30] docs: clarify Kimi install roots --- bin/install.js | 4 ++-- docs/how-to/install-on-your-runtime.md | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bin/install.js b/bin/install.js index e67bb5168..17fe3d9b6 100755 --- a/bin/install.js +++ b/bin/install.js @@ -627,7 +627,7 @@ const banner = '\n' + ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' + ' Git. Ship. Done.\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development workflows for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy.\n'; + ' development workflows for Claude Code, OpenCode, Gemini, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy.\n'; // Pure seam: parse --config-dir / -c from an arbitrary args array. // Returns the path string, '' for an empty equals-form value, or null when the @@ -684,7 +684,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.config/agents${reset}; use ${cyan}--config-dir ~/.kimi${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi${reset} for brand-specific Kimi installs.\n`); process.exit(0); } diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 5fad4b8d3..6edd6f2cb 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -124,12 +124,30 @@ Kimi custom agents do not auto-activate just because the files exist. Launch Kim kimi --agent-file ~/.config/agents/agents/gsd.yaml ``` +GSD uses Kimi's generic Agent Skills root (`~/.config/agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi` directory. If your Kimi setup is already centered on `~/.kimi`, install there explicitly: + +```bash +npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi +``` + +Then launch the generated agent from that directory: + +```bash +kimi --agent-file ~/.kimi/agents/gsd.yaml +``` + **Override the install directory:** ```bash KIMI_CONFIG_DIR=~/.config/agents-alt npx @opengsd/gsd-core@latest --kimi --global ``` +For brand-specific scripted installs, use: + +```bash +KIMI_CONFIG_DIR=~/.kimi npx @opengsd/gsd-core@latest --kimi --global +``` + `--kimi --local` is intentionally deferred and guarded in v1; use the global install path above for Kimi CLI. --- From 40c3fec397727180c7e9492a2c1784544ec6e967 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:27:12 +0200 Subject: [PATCH 23/30] fix: align kimi runtime support with review --- .changeset/kimi-runtime-support.md | 2 +- bin/install.js | 135 +++++++++------ docs/ARCHITECTURE.md | 14 +- docs/how-to/install-on-your-runtime.md | 16 +- src/runtime-homes.cts | 11 +- .../bug-kimi-path-layout-local-guard.test.cjs | 161 ++++++++++++++++-- tests/helpers/install-shared.cjs | 2 +- ...ler-migration-install-integration.test.cjs | 2 +- tests/kimi-tool-mapping.test.cjs | 3 + tests/multi-runtime-select.test.cjs | 2 + 10 files changed, 257 insertions(+), 91 deletions(-) diff --git a/.changeset/kimi-runtime-support.md b/.changeset/kimi-runtime-support.md index 29f265ac5..2aa33a6e7 100644 --- a/.changeset/kimi-runtime-support.md +++ b/.changeset/kimi-runtime-support.md @@ -1,5 +1,5 @@ --- type: Added -pr: 817 +pr: 743 --- **Kimi CLI runtime support is now documented and installable** — users can install global GSD Agent Skills with `--kimi --global`, invoke them as `/skill:gsd-*`, and launch the generated custom agent explicitly with `kimi --agent-file`. diff --git a/bin/install.js b/bin/install.js index 17fe3d9b6..fc983ee33 100755 --- a/bin/install.js +++ b/bin/install.js @@ -346,7 +346,7 @@ function getDirName(runtime) { if (runtime === 'trae') return '.trae'; if (runtime === 'qwen') return '.qwen'; if (runtime === 'hermes') return '.hermes'; - if (runtime === 'kimi') return '.kimi'; + if (runtime === 'kimi') return '.kimi-code'; if (runtime === 'codebuddy') return '.codebuddy'; if (runtime === 'cline') return '.cline'; return '.claude'; @@ -393,7 +393,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'hermes') return "'.hermes'"; if (runtime === 'codebuddy') return "'.codebuddy'"; if (runtime === 'cline') return "'.cline'"; - if (runtime === 'kimi') return "'.config', 'agents'"; + if (runtime === 'kimi') return "'.agents'"; return "'.claude'"; } @@ -684,7 +684,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.config/agents${reset}; use ${cyan}--config-dir ~/.kimi${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi${reset} for brand-specific Kimi installs.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.agents${reset}; use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); process.exit(0); } @@ -1892,9 +1892,9 @@ const claudeToKimiTools = { WebFetch: 'kimi_cli.tools.web:FetchURL', FetchURL: 'kimi_cli.tools.web:FetchURL', ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile', - TaskList: 'kimi_cli.tools.task:TaskList', - TaskOutput: 'kimi_cli.tools.task:TaskOutput', - TaskStop: 'kimi_cli.tools.task:TaskStop', + TaskList: 'kimi_cli.tools.background:TaskList', + TaskOutput: 'kimi_cli.tools.background:TaskOutput', + TaskStop: 'kimi_cli.tools.background:TaskStop', }; /** @@ -6539,7 +6539,7 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkAndRewrite(fullPath); - } else if (entry.name === 'SKILL.md') { + } else if (entry.name.endsWith('.md')) { let content = fs.readFileSync(fullPath, 'utf8'); content = _applyRuntimeRewrites(content, runtime, pathPrefix); fs.writeFileSync(fullPath, content); @@ -6667,6 +6667,16 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { content = processAttribution(content, getCommitAttribution(runtime)); break; + case 'kimi': + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${dirName}/`); + content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); + content = processAttribution(content, getCommitAttribution(runtime)); + break; + default: // Unknown runtime — no rewrites break; @@ -6952,7 +6962,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { for (const kind of layout.kinds) { const staged = kind.stage(resolvedProfile); - if (kind.kind === 'skills') { + if (kind.kind === 'skills' || kind.kind === 'kimi-agents') { applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix); } const dest = path.join(layout.configDir, kind.destSubpath); @@ -7093,6 +7103,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = content.replace(globalClaudeRegex, pathPrefix); content = content.replace(globalClaudeHomeRegex, pathPrefix); content = content.replace(localClaudeRegex, `./${dirName}/`); + content = content.replace(/~\/\.claude\b/g, pathPrefix.replace(/\/$/, '')); + content = content.replace(/\$HOME\/\.claude\b/g, pathPrefix.replace(/\/$/, '')); + content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); content = content.replace(/~\/\.qwen\//g, pathPrefix); content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix); content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`); @@ -7418,6 +7431,7 @@ function uninstall(isGlobal, runtime = 'claude') { if (runtime === 'trae') runtimeLabel = 'Trae'; if (runtime === 'qwen') runtimeLabel = 'Qwen Code'; if (runtime === 'hermes') runtimeLabel = 'Hermes Agent'; + if (runtime === 'kimi') runtimeLabel = 'Kimi CLI'; if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy'; console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); @@ -8194,6 +8208,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { const isWindsurf = runtime === 'windsurf'; const isTrae = runtime === 'trae'; const isCline = runtime === 'cline'; + const isKimi = runtime === 'kimi'; const isHermes = runtime === 'hermes'; const gsdDir = path.join(configDir, 'gsd-core'); const commandsDir = path.join(configDir, 'commands', 'gsd'); @@ -8258,7 +8273,16 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { } } } - if (fs.existsSync(agentsDir)) { + if (isKimi && fs.existsSync(agentsDir)) { + const agentHashes = generateManifest(agentsDir); + for (const [rel, hash] of Object.entries(agentHashes)) { + const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md'; + const isSubagent = /^subagents\/gsd-[^/]+\.(yaml|md)$/.test(rel); + if (isRootAgent || isSubagent) { + manifest.files['agents/' + rel] = hash; + } + } + } else if (fs.existsSync(agentsDir)) { for (const file of fs.readdirSync(agentsDir)) { if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) { manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file)); @@ -8275,7 +8299,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { // Track hook files so saveLocalPatches() can detect user modifications // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline) - if (!isCodex && !isCopilot && !isCline) { + if (!isCodex && !isCopilot && !isCline && !isKimi) { const hooksDir = path.join(configDir, 'hooks'); if (fs.existsSync(hooksDir)) { for (const file of fs.readdirSync(hooksDir)) { @@ -8640,7 +8664,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { if (isKimi && !isGlobal) { console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`); - console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); + console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`); console.log(` Project-level Kimi install semantics remain deferred.`); return { runtime, @@ -9044,40 +9068,6 @@ function install(isGlobal, runtime = 'claude', options = {}) { reportInstallerMigrationResult(installerMigrationResult); assertInstallerMigrationsUnblocked(installerMigrationResult); - if (isKimi && isGlobal) { - installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); - const skillsDir = path.join(targetDir, 'skills'); - const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); - const count = fs.existsSync(skillsDir) - ? fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length - : 0; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); - } else { - throw new Error('Kimi global install produced no skills/gsd-* entries'); - } - if (fs.existsSync(rootAgentPath)) { - console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); - console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); - } else { - throw new Error('Kimi global install produced no agents/gsd.yaml'); - } - console.log(`\n ${green}Done!${reset} Launch Kimi with ${cyan}kimi --agent-file ${rootAgentPath}${reset}.`); - return { - runtime, - skipped: true, - reason: 'kimi_global_skills_and_agents', - configDir: targetDir, - agentPath: rootAgentPath, - settingsPath: null, - settings: null, - statuslineCommand: null, - updateBannerCommand: null, - rollbackInstallerMigrations, - }; - } - // Artifact install dispatcher — routes to layout-driven path for all // skills-based runtimes (both full and minimal/core profiles); keeps // back-compat paths for commands-based runtimes (OpenCode/Kilo/Gemini/ @@ -9127,7 +9117,25 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('skills/gsd/*'); } } else if (isKimi) { - console.log(` ${yellow}⚠${reset} Kimi SKILL.md conversion is deferred for Phase 2; no skills were written.`); + const skillsDir = path.join(targetDir, 'skills'); + const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); + if (fs.existsSync(skillsDir)) { + const count = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length; + if (count > 0) { + console.log(` ${green}✓${reset} Installed ${count} Kimi skills to skills/`); + } else { + failures.push('skills/gsd-*'); + } + } else { + failures.push('skills/gsd-*'); + } + if (fs.existsSync(rootAgentPath)) { + console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); + console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); + } else { + failures.push('agents/gsd.yaml'); + } } else { const skillsDir = path.join(targetDir, 'skills'); if (fs.existsSync(skillsDir)) { @@ -9305,7 +9313,9 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } - if (isMinimalMode(_effectiveInstallMode)) { + if (isKimi) { + console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`); + } else if (isMinimalMode(_effectiveInstallMode)) { // Codex registers agents in `config.toml` via `[agents.gsd-*]` sections. // Without stripping them here, a full → minimal reinstall would leave the // runtime advertising the old full agent surface even though the agent @@ -9448,7 +9458,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('VERSION'); } - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) { + if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) { // Write package.json to force CommonJS mode for GSD scripts // Prevents "require is not defined" errors when project has "type": "module" // Node.js walks up looking for package.json - this stops inheritance from project @@ -9542,7 +9552,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // receive the hooks/lib/ helpers either — otherwise the Codex comment downstream // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice. const hooksLibSrc = path.join(src, 'hooks', 'lib'); - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && fs.existsSync(hooksLibSrc)) { + if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) { const hooksLibDest = path.join(targetDir, 'hooks', 'lib'); fs.mkdirSync(hooksLibDest, { recursive: true }); copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES); @@ -10023,6 +10033,13 @@ function install(isGlobal, runtime = 'claude', options = {}) { return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } + if (isKimi) { + // Kimi uses Agent Skills plus explicit custom agent YAML files. It does + // not own settings.json, hooks, rules, or update-banner/statusline config. + persistActiveProfileMarker(); + return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; + } + if (isCline) { // Cline uses .clinerules — generate a rules file with GSD system instructions const clinerulesDest = path.join(targetDir, '.clinerules'); @@ -10579,8 +10596,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS const isWindsurf = runtime === 'windsurf'; const isTrae = runtime === 'trae'; const isCline = runtime === 'cline'; + const isKimi = runtime === 'kimi'; - if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) { + if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isKimi) { if (!isGlobal && !forceStatusline) { // Local installs skip statusLine by default: repo settings.json takes precedence over // profile-level settings.json in Claude Code, so writing here would silently clobber @@ -10638,7 +10656,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS // {type: 'command', command: null} items that the runtime hook schema // rejects at parse time. validateHookFields filters those out so the file // we write is always schema-valid. - if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) { + if (settingsPath && settings && !isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) { writeSettings(settingsPath, validateHookFields(settings)); } @@ -10687,6 +10705,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'cline') program = 'Cline'; if (runtime === 'qwen') program = 'Qwen Code'; if (runtime === 'hermes') program = 'Hermes Agent'; + if (runtime === 'kimi') program = 'Kimi CLI'; let command = '/gsd-new-project'; if (runtime === 'opencode') command = '/gsd-new-project'; @@ -10702,6 +10721,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'cline') command = '/gsd-new-project'; if (runtime === 'qwen') command = '/gsd-new-project'; if (runtime === 'hermes') command = '/gsd-new-project'; + if (runtime === 'kimi') command = '/skill:gsd-new-project'; // Claude Code global installs use the skills/ format (CC 2.1.88+). // Restart is required for CC to pick up newly-installed skills, and the @@ -10716,6 +10736,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS return; } + if (runtime === 'kimi') { + const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml'; + console.log(` + ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}. + + ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r +`); + return; + } + console.log(` ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}. @@ -10818,7 +10848,7 @@ function buildRuntimePromptText() { ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset} ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset} ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset} - ${cyan}11${reset}) Kimi ${dim}(~/.config/agents)${reset} + ${cyan}11${reset}) Kimi ${dim}(~/.agents)${reset} ${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset} ${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset} ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset} @@ -11288,6 +11318,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const printSummaries = () => { for (const result of results) { if (result && result.skipped) continue; + if (!result) continue; const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; finishInstall( result.settingsPath, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0d9defddc..f16a02572 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -563,7 +563,7 @@ Equivalent paths for other runtimes: - **OpenCode:** `~/.config/opencode/` global or `./.opencode/` local - **Kilo:** `~/.config/kilo/` global or `./.kilo/` local - **Gemini CLI:** `~/.gemini/` global or `./.gemini/` local -- **Kimi CLI:** `~/.config/agents/` global; local install is deferred and guarded +- **Kimi CLI:** `~/.agents/` global; local install is deferred and guarded - **Codex:** `~/.codex/` global or `./.codex/` local - **Copilot:** `~/.copilot/` global or `./.github/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local @@ -802,7 +802,7 @@ The migration-specific ownership and source snapshots live in | OpenCode | `~/.config/opencode` | `./.opencode` | `command/gsd-*.md` | `agents/gsd-*.md` | `opencode.json` or `opencode.jsonc`; no GSD hooks | | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Gemini CLI | `~/.gemini` | `./.gemini` | `commands/gsd/*.toml` | `agents/gsd-*.md` | `settings.json` feature flag, hooks, and statusline | -| Kimi CLI | `~/.config/agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.config/agents/agents/gsd.yaml`; no GSD hooks or statusline | +| Kimi CLI | `~/.agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.agents/agents/gsd.yaml`; no GSD hooks or statusline | | Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` and `copilot-instructions.md` | `.agent.md` files | No GSD hooks or statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | @@ -818,15 +818,17 @@ The migration-specific ownership and source snapshots live in ### Upstream Contract Sources Runtime install expectations are checked against primary documentation where -available. The current source snapshot is 2026-05-11: +available. The current source snapshot is 2026-05-11, with Kimi CLI rechecked +on 2026-06-06: - Claude Code: Anthropic slash commands, settings, hooks, and subagents docs. - OpenCode and Kilo: OpenCode config docs and Kilo custom subagent docs. - Gemini CLI and Qwen Code: command/config docs; Qwen command docs were last updated 2026-05-06. -- Kimi CLI: Agent Skills docs for `~/.config/agents/skills/` discovery and - Agents docs for YAML files, `system_prompt_path`, `kimi_cli.tools.*` module - paths, and explicit `kimi --agent-file` launch. +- Kimi CLI: Agent Skills docs for `~/.kimi-code/skills/` and + `~/.agents/skills/` user-level discovery, plus Agents docs for YAML files, + `system_prompt_path`, `kimi_cli.tools.*` module paths, and explicit + `kimi --agent-file` launch. - Codex: OpenAI Codex docs and `config-schema.json`; the installer also carries Codex 0.124.0 compatibility for agent table shape. - Copilot, Cursor, Cline, Augment, Hermes, and CodeBuddy: vendor docs for diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 6edd6f2cb..cfad88a00 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -110,42 +110,42 @@ Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written with per-age npx @opengsd/gsd-core@latest --kimi --global ``` -Skills land in `~/.config/agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: +Skills land in `~/.agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: ```text /skill:gsd-new-project ``` -The installer also writes the GSD custom agent definition to `~/.config/agents/agents/gsd.yaml` with its prompt at `~/.config/agents/agents/gsd.md`; subagents land under `~/.config/agents/agents/subagents/gsd-*.yaml` and `~/.config/agents/agents/subagents/gsd-*.md`. +The installer also writes the GSD custom agent definition to `~/.agents/agents/gsd.yaml` with its prompt at `~/.agents/agents/gsd.md`; subagents land under `~/.agents/agents/subagents/gsd-*.yaml` and `~/.agents/agents/subagents/gsd-*.md`. Kimi custom agents do not auto-activate just because the files exist. Launch Kimi with the generated agent file when you want the GSD agent surface: ```bash -kimi --agent-file ~/.config/agents/agents/gsd.yaml +kimi --agent-file ~/.agents/agents/gsd.yaml ``` -GSD uses Kimi's generic Agent Skills root (`~/.config/agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi` directory. If your Kimi setup is already centered on `~/.kimi`, install there explicitly: +GSD uses Kimi's generic Agent Skills root (`~/.agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi-code` directory. If your Kimi setup is already centered on `~/.kimi-code`, install there explicitly: ```bash -npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi +npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi-code ``` Then launch the generated agent from that directory: ```bash -kimi --agent-file ~/.kimi/agents/gsd.yaml +kimi --agent-file ~/.kimi-code/agents/gsd.yaml ``` **Override the install directory:** ```bash -KIMI_CONFIG_DIR=~/.config/agents-alt npx @opengsd/gsd-core@latest --kimi --global +KIMI_CONFIG_DIR=~/.agents-alt npx @opengsd/gsd-core@latest --kimi --global ``` For brand-specific scripted installs, use: ```bash -KIMI_CONFIG_DIR=~/.kimi npx @opengsd/gsd-core@latest --kimi --global +KIMI_CONFIG_DIR=~/.kimi-code npx @opengsd/gsd-core@latest --kimi --global ``` `--kimi --local` is intentionally deferred and guarded in v1; use the global install path above for Kimi CLI. diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index a775726cf..30ac2cd26 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -14,9 +14,9 @@ * cline — Rules-based; commands are embedded in .clinerules. Cline does * not use a skills/ directory. getGlobalSkillDir() returns null * for cline so the caller can emit an appropriate warning. - * kimi — Agent Skills are discovered from the generic agents root: - * ~/.config/agents/skills. ~/.kimi/skills is compatible with - * Kimi CLI, but is not GSD's canonical Phase 1 install target. + * kimi — Agent Skills are discovered from Kimi's generic user root: + * ~/.agents/skills. ~/.kimi-code/skills is compatible with + * Kimi Code CLI and can be selected with KIMI_CONFIG_DIR. */ import os from 'node:os'; @@ -128,11 +128,10 @@ export function getGlobalConfigDir(runtime: string): string { case 'cline': return env['CLINE_CONFIG_DIR'] ? expandTilde(env['CLINE_CONFIG_DIR']) : path.join(home, '.cline'); - // ── Kimi CLI (generic agents XDG root) ───────────────────────────────── + // ── Kimi CLI (generic agents user root) ──────────────────────────────── case 'kimi': { if (env['KIMI_CONFIG_DIR']) return expandTilde(env['KIMI_CONFIG_DIR']); - if (env['XDG_CONFIG_HOME']) return path.join(expandTilde(env['XDG_CONFIG_HOME']), 'agents'); - return path.join(home, '.config', 'agents'); + return path.join(home, '.agents'); } // ── OpenCode (XDG) ─────────────────────────────────────────────────────── diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 40483a4d9..0d6fe1668 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -47,47 +47,47 @@ function withEnv(updates, fn) { } describe('Kimi runtime homes', () => { - test('canonical global skills base is ~/.config/agents/skills, not ~/.kimi/skills', () => { + test('canonical global skills base is ~/.agents/skills, not ~/.kimi-code/skills', () => { withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: undefined }, () => { assert.strictEqual( getGlobalConfigDir('kimi'), - path.join(os.homedir(), '.config', 'agents'), + path.join(os.homedir(), '.agents'), ); assert.strictEqual( getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.config', 'agents', 'skills'), + path.join(os.homedir(), '.agents', 'skills'), ); assert.strictEqual( getGlobalSkillDir('kimi', 'gsd-help'), - path.join(os.homedir(), '.config', 'agents', 'skills', 'gsd-help'), + path.join(os.homedir(), '.agents', 'skills', 'gsd-help'), ); assert.notStrictEqual( getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.kimi', 'skills'), + path.join(os.homedir(), '.kimi-code', 'skills'), ); }); }); - test('KIMI_CONFIG_DIR overrides the Kimi generic agents config root', () => { - withEnv({ KIMI_CONFIG_DIR: '/tmp/custom-kimi-agents', XDG_CONFIG_HOME: undefined }, () => { - assert.strictEqual(getGlobalConfigDir('kimi'), '/tmp/custom-kimi-agents'); + test('KIMI_CONFIG_DIR can select the brand-specific ~/.kimi-code root', () => { + withEnv({ KIMI_CONFIG_DIR: '/tmp/custom-kimi-code', XDG_CONFIG_HOME: undefined }, () => { + assert.strictEqual(getGlobalConfigDir('kimi'), '/tmp/custom-kimi-code'); assert.strictEqual( getGlobalSkillsBase('kimi'), - path.join('/tmp/custom-kimi-agents', 'skills'), + path.join('/tmp/custom-kimi-code', 'skills'), ); - assert.strictEqual(getGlobalDir('kimi'), '/tmp/custom-kimi-agents'); + assert.strictEqual(getGlobalDir('kimi'), '/tmp/custom-kimi-code'); }); }); - test('XDG_CONFIG_HOME participates in the default Kimi generic agents path', () => { + test('XDG_CONFIG_HOME does not change Kimi default root', () => { withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/xdg-home' }, () => { assert.strictEqual( getGlobalConfigDir('kimi'), - path.join('/tmp/xdg-home', 'agents'), + path.join(os.homedir(), '.agents'), ); assert.strictEqual( getConfigDirFromHome('kimi', true), - "'.config', 'agents'", + "'.agents'", ); }); }); @@ -138,7 +138,8 @@ describe('Kimi local install guard', () => { assert.match(combined, /Kimi local install/i); assert.match(combined, /deferred/i); - assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi')), 'must not create .kimi/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi')), 'must not create legacy .kimi/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi-code')), 'must not create .kimi-code/'); assert.ok(!fs.existsSync(path.join(tmpProject, '.agents')), 'must not create .agents/'); assert.ok(!fs.existsSync(path.join(tmpProject, '.claude')), 'must not fall back to Claude local install'); } finally { @@ -169,15 +170,19 @@ describe('Kimi local install guard', () => { ); const combined = `${result.stdout}\n${result.stderr}`; assert.match(combined, /Installing for .*Kimi/i); - assert.match(combined, /Installed \d+ skills to skills\//i); + assert.match(combined, /Installed \d+ Kimi skills to skills\//i); assert.match(combined, /Generated Kimi root agent: .*agents.*gsd\.yaml/i); assert.match(combined, /kimi --agent-file/i); + assert.match(combined, /Wrote file manifest/i); const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); assert.ok(fs.existsSync(skillFile), 'must write gsd-new-project/SKILL.md'); const skillContent = fs.readFileSync(skillFile, 'utf8'); assert.match(skillContent, /^name: gsd-new-project$/m); assert.match(skillContent, /\/skill:gsd-new-project/); + assert.match(skillContent, /gsd-core\/workflows\/new-project\.md/); + assert.doesNotMatch(skillContent, /@~\/\.claude\/gsd-core|@\$HOME\/\.claude\/gsd-core/); + assert.doesNotMatch(skillContent, /@[^\r\n]*\\/, 'serialized Kimi payload references must use forward slashes'); assert.doesNotMatch(skillContent, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); const rootYaml = path.join(tmpConfig, 'agents', 'gsd.yaml'); @@ -204,8 +209,132 @@ describe('Kimi local install guard', () => { assert.match(executorYamlContent, /kimi_cli\.tools\./); assert.doesNotMatch(executorYamlContent, /mcp__/); - assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts'); + assert.ok(fs.existsSync(path.join(tmpConfig, 'gsd-core', 'workflows', 'new-project.md')), 'must write workflow payloads used by Kimi skills'); + const manifestPath = path.join(tmpConfig, 'gsd-file-manifest.json'); + assert.ok(fs.existsSync(manifestPath), 'must write gsd-file-manifest.json for Kimi installs'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + assert.ok(manifest.files['skills/gsd-new-project/SKILL.md'], 'manifest tracks generated Kimi skill'); + assert.ok(manifest.files['agents/gsd.yaml'], 'manifest tracks Kimi root agent YAML'); + assert.ok(manifest.files['agents/gsd.md'], 'manifest tracks Kimi root agent prompt'); + assert.ok(manifest.files['agents/subagents/gsd-executor.yaml'], 'manifest tracks Kimi subagent YAML'); + assert.ok(manifest.files['agents/subagents/gsd-executor.md'], 'manifest tracks Kimi subagent prompt'); + assert.ok(manifest.files['gsd-core/workflows/new-project.md'], 'manifest tracks installed workflow payload'); assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'settings.json')), 'must not write settings.json under the Kimi root'); + assert.ok(!fs.existsSync(path.join(tmpConfig, '.clinerules')), 'must not write rules under the Kimi root'); + } finally { + cleanup(tmpProject); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); + + test('--kimi --global backs up local edits to generated skills and agent artifacts on reinstall', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-project-')); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-home-')); + const env = installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }); + + try { + const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk']; + const first = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + first.status, + 0, + `first install failed\nstdout: ${first.stdout}\nstderr: ${first.stderr}`, + ); + + const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); + const agentPrompt = path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.md'); + fs.appendFileSync(skillFile, '\nUSER LOCAL KIMI SKILL EDIT\n'); + fs.appendFileSync(agentPrompt, '\nUSER LOCAL KIMI AGENT EDIT\n'); + + const second = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + second.status, + 0, + `second install failed\nstdout: ${second.stdout}\nstderr: ${second.stderr}`, + ); + assert.match(`${second.stdout}\n${second.stderr}`, /locally modified GSD file/i); + + const skillBackup = path.join(tmpConfig, 'gsd-local-patches', 'skills', 'gsd-new-project', 'SKILL.md'); + const agentBackup = path.join(tmpConfig, 'gsd-local-patches', 'agents', 'subagents', 'gsd-executor.md'); + assert.match(fs.readFileSync(skillBackup, 'utf8'), /USER LOCAL KIMI SKILL EDIT/); + assert.match(fs.readFileSync(agentBackup, 'utf8'), /USER LOCAL KIMI AGENT EDIT/); + + const meta = JSON.parse(fs.readFileSync(path.join(tmpConfig, 'gsd-local-patches', 'backup-meta.json'), 'utf8')); + assert.ok(meta.files.includes('skills/gsd-new-project/SKILL.md')); + assert.ok(meta.files.includes('agents/subagents/gsd-executor.md')); + } finally { + cleanup(tmpProject); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); + + test('--kimi --global --uninstall removes GSD artifacts and preserves non-GSD Kimi content', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-project-')); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-home-')); + const env = installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }); + + try { + const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk']; + const installResult = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + installResult.status, + 0, + `install failed\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`, + ); + + const foreignSkill = path.join(tmpConfig, 'skills', 'user-custom-skill', 'SKILL.md'); + const foreignRootAgent = path.join(tmpConfig, 'agents', 'user-agent.yaml'); + const foreignSubagent = path.join(tmpConfig, 'agents', 'subagents', 'user-agent.yaml'); + fs.mkdirSync(path.dirname(foreignSkill), { recursive: true }); + fs.mkdirSync(path.dirname(foreignSubagent), { recursive: true }); + fs.writeFileSync(foreignSkill, '# user skill\n', 'utf8'); + fs.writeFileSync(foreignRootAgent, 'version: 1\n', 'utf8'); + fs.writeFileSync(foreignSubagent, 'version: 1\n', 'utf8'); + + const uninstallResult = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--uninstall'], + { + cwd: tmpProject, + encoding: 'utf8', + env, + }, + ); + assert.strictEqual( + uninstallResult.status, + 0, + `uninstall failed\nstdout: ${uninstallResult.stdout}\nstderr: ${uninstallResult.stderr}`, + ); + assert.match(`${uninstallResult.stdout}\n${uninstallResult.stderr}`, /Kimi CLI/); + + assert.ok(!fs.existsSync(path.join(tmpConfig, 'skills', 'gsd-new-project')), 'must remove generated Kimi skills'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'gsd.yaml')), 'must remove Kimi root agent YAML'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'gsd.md')), 'must remove Kimi root agent prompt'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.yaml')), 'must remove generated Kimi subagent YAML'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.md')), 'must remove generated Kimi subagent prompt'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must remove installed workflow payload'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-file-manifest.json')), 'must remove the manifest'); + + assert.ok(fs.existsSync(foreignSkill), 'must preserve non-GSD Kimi skills'); + assert.ok(fs.existsSync(foreignRootAgent), 'must preserve non-GSD root agents'); + assert.ok(fs.existsSync(foreignSubagent), 'must preserve non-GSD subagents'); } finally { cleanup(tmpProject); cleanup(tmpConfig); diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 12c580d91..fb499452b 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -48,7 +48,7 @@ const RUNTIME_META = { cursor: { localDir: '.cursor', globalSuffix: '.cursor' }, gemini: { localDir: '.gemini', globalSuffix: '.gemini' }, hermes: { localDir: '.hermes', globalSuffix: '.hermes' }, - kimi: { localDir: '.kimi', globalSuffix: path.join('.config', 'agents') }, + kimi: { localDir: '.kimi-code', globalSuffix: '.agents' }, kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, diff --git a/tests/installer-migration-install-integration.test.cjs b/tests/installer-migration-install-integration.test.cjs index 304d416b0..ae047122c 100644 --- a/tests/installer-migration-install-integration.test.cjs +++ b/tests/installer-migration-install-integration.test.cjs @@ -34,7 +34,7 @@ const RUNTIME_INSTALL_CONTRACTS = { cursor: { surface: 'flat-skills', settings: false, packageJson: false }, gemini: { surface: 'commands-gsd', settings: true, packageJson: true }, hermes: { surface: 'hermes-skills', settings: true, packageJson: true }, - kimi: { surface: 'kimi-skills-agents', settings: false, packageJson: false, workflowPayload: false }, + kimi: { surface: 'kimi-skills-agents', settings: false, packageJson: false }, kilo: { surface: 'flat-command', settings: false, packageJson: true }, opencode: { surface: 'flat-command', settings: true, packageJson: true }, qwen: { surface: 'flat-skills', settings: true, packageJson: true }, diff --git a/tests/kimi-tool-mapping.test.cjs b/tests/kimi-tool-mapping.test.cjs index 324dfc4d3..ef1692f0e 100644 --- a/tests/kimi-tool-mapping.test.cjs +++ b/tests/kimi-tool-mapping.test.cjs @@ -32,6 +32,9 @@ describe('convertKimiToolName', () => { ['TodoWrite', 'kimi_cli.tools.todo:SetTodoList'], ['WebSearch', 'kimi_cli.tools.web:SearchWeb'], ['WebFetch', 'kimi_cli.tools.web:FetchURL'], + ['TaskList', 'kimi_cli.tools.background:TaskList'], + ['TaskOutput', 'kimi_cli.tools.background:TaskOutput'], + ['TaskStop', 'kimi_cli.tools.background:TaskStop'], ]); for (const [claudeTool, kimiTool] of expectedMappings) { diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 8b2efa530..771440a52 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -187,6 +187,8 @@ describe('install.js exports multi-select runtime metadata', () => { 'prompt lists Hermes Agent as option 10'); assert.ok(/\b11\)\s*Kimi\b/.test(prompt), 'prompt lists Kimi as option 11'); + assert.ok(/Kimi\s+\(~\/\.agents\)/.test(prompt), + 'prompt shows the canonical Kimi global root'); assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt), 'prompt lists Qwen Code as option 14'); assert.ok(/\b15\)\s*Trae\b/.test(prompt), From 2d9e44c6a633b92491b1923182cad6a4e23015f9 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:05:13 +0200 Subject: [PATCH 24/30] fix: align kimi install roots with docs --- bin/install.js | 6 +- docs/ARCHITECTURE.md | 14 +-- docs/how-to/install-on-your-runtime.md | 17 ++- docs/installer-migrations.md | 4 +- src/runtime-homes.cts | 45 +++++++- .../bug-kimi-path-layout-local-guard.test.cjs | 109 +++++++++++++++--- tests/helpers/install-shared.cjs | 2 +- tests/multi-runtime-select.test.cjs | 4 +- 8 files changed, 160 insertions(+), 41 deletions(-) diff --git a/bin/install.js b/bin/install.js index 98504a91e..17bede13c 100755 --- a/bin/install.js +++ b/bin/install.js @@ -397,7 +397,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'hermes') return "'.hermes'"; if (runtime === 'codebuddy') return "'.codebuddy'"; if (runtime === 'cline') return "'.cline'"; - if (runtime === 'kimi') return "'.agents'"; + if (runtime === 'kimi') return "'.config', 'agents'"; return "'.claude'"; } @@ -688,7 +688,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.agents${reset}; use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, GSD creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); process.exit(0); } @@ -10923,7 +10923,7 @@ function buildRuntimePromptText() { ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset} ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset} ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset} - ${cyan}11${reset}) Kimi ${dim}(~/.agents)${reset} + ${cyan}11${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset} ${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset} ${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset} ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f16a02572..61e8d187e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -563,7 +563,7 @@ Equivalent paths for other runtimes: - **OpenCode:** `~/.config/opencode/` global or `./.opencode/` local - **Kilo:** `~/.config/kilo/` global or `./.kilo/` local - **Gemini CLI:** `~/.gemini/` global or `./.gemini/` local -- **Kimi CLI:** `~/.agents/` global; local install is deferred and guarded +- **Kimi CLI:** first-existing generic global root (`~/.config/agents/` recommended, then `~/.agents/` if its `skills/` directory already exists); local install is deferred and guarded - **Codex:** `~/.codex/` global or `./.codex/` local - **Copilot:** `~/.copilot/` global or `./.github/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local @@ -802,7 +802,7 @@ The migration-specific ownership and source snapshots live in | OpenCode | `~/.config/opencode` | `./.opencode` | `command/gsd-*.md` | `agents/gsd-*.md` | `opencode.json` or `opencode.jsonc`; no GSD hooks | | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Gemini CLI | `~/.gemini` | `./.gemini` | `commands/gsd/*.toml` | `agents/gsd-*.md` | `settings.json` feature flag, hooks, and statusline | -| Kimi CLI | `~/.agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.agents/agents/gsd.yaml`; no GSD hooks or statusline | +| Kimi CLI | First-existing generic root: `~/.config/agents` recommended, then `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file /agents/gsd.yaml`; no GSD hooks or statusline | | Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` and `copilot-instructions.md` | `.agent.md` files | No GSD hooks or statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | @@ -819,16 +819,16 @@ The migration-specific ownership and source snapshots live in Runtime install expectations are checked against primary documentation where available. The current source snapshot is 2026-05-11, with Kimi CLI rechecked -on 2026-06-06: +on 2026-06-07: - Claude Code: Anthropic slash commands, settings, hooks, and subagents docs. - OpenCode and Kilo: OpenCode config docs and Kilo custom subagent docs. - Gemini CLI and Qwen Code: command/config docs; Qwen command docs were last updated 2026-05-06. -- Kimi CLI: Agent Skills docs for `~/.kimi-code/skills/` and - `~/.agents/skills/` user-level discovery, plus Agents docs for YAML files, - `system_prompt_path`, `kimi_cli.tools.*` module paths, and explicit - `kimi --agent-file` launch. +- Kimi CLI: Agent Skills docs for user-level brand roots and first-existing + generic roots (`~/.config/agents/skills/` recommended, then + `~/.agents/skills/`), plus Agents docs for YAML files, `system_prompt_path`, + `kimi_cli.tools.*` module paths, and explicit `kimi --agent-file` launch. - Codex: OpenAI Codex docs and `config-schema.json`; the installer also carries Codex 0.124.0 compatibility for agent table shape. - Copilot, Cursor, Cline, Augment, Hermes, and CodeBuddy: vendor docs for diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index cfad88a00..55074ba5f 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -110,21 +110,32 @@ Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written with per-age npx @opengsd/gsd-core@latest --kimi --global ``` -Skills land in `~/.agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: +Skills land in Kimi's first existing generic user skills root: + +- `~/.config/agents/skills/gsd-*/SKILL.md` when `~/.config/agents/skills` already exists, or when neither generic root exists yet +- `~/.agents/skills/gsd-*/SKILL.md` when `~/.agents/skills` already exists and `~/.config/agents/skills` does not + +Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: ```text /skill:gsd-new-project ``` -The installer also writes the GSD custom agent definition to `~/.agents/agents/gsd.yaml` with its prompt at `~/.agents/agents/gsd.md`; subagents land under `~/.agents/agents/subagents/gsd-*.yaml` and `~/.agents/agents/subagents/gsd-*.md`. +The installer also writes the GSD custom agent definition to the same selected config root: `/agents/gsd.yaml` with its prompt at `/agents/gsd.md`; subagents land under `/agents/subagents/gsd-*.yaml` and `/agents/subagents/gsd-*.md`. Kimi custom agents do not auto-activate just because the files exist. Launch Kimi with the generated agent file when you want the GSD agent surface: +```bash +kimi --agent-file ~/.config/agents/agents/gsd.yaml +``` + +If your machine already uses `~/.agents/skills` and does not have `~/.config/agents/skills`, GSD installs there instead and the launch command becomes: + ```bash kimi --agent-file ~/.agents/agents/gsd.yaml ``` -GSD uses Kimi's generic Agent Skills root (`~/.agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi-code` directory. If your Kimi setup is already centered on `~/.kimi-code`, install there explicitly: +Kimi also discovers user skills from the brand-specific `~/.kimi-code` directory. If your Kimi setup is already centered on `~/.kimi-code`, install there explicitly: ```bash npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi-code diff --git a/docs/installer-migrations.md b/docs/installer-migrations.md index 88f4e96b0..7de591e25 100644 --- a/docs/installer-migrations.md +++ b/docs/installer-migrations.md @@ -345,7 +345,8 @@ only the owned portion. ## Runtime Configuration Contract Registry -Last upstream documentation check: 2026-05-11. +Last upstream documentation check: 2026-05-11. Kimi CLI was rechecked on +2026-06-07 against the MoonshotAI docs. This registry is the source of truth for migrations that touch host runtime configuration. Each row records: @@ -367,6 +368,7 @@ for the new shape before changing migration behavior. | OpenCode | Flat markdown commands in `command/gsd-*.md`; agents in `agents/gsd-*.md`; config updates in `opencode.json` or `opencode.jsonc` | Global `OPENCODE_CONFIG_DIR`, `dirname(OPENCODE_CONFIG)`, `XDG_CONFIG_HOME/opencode`, or `~/.config/opencode`; local `./.opencode` | GSD owns generated command/agent files and GSD entries in structured config only | [Config](https://opencode.ai/docs/config/); docs published 2026-05, checked 2026-05-11 | | Kilo | OpenCode-style flat markdown commands in `command/gsd-*.md`; agents in `agents/gsd-*.md`; config updates in `kilo.json` or `kilo.jsonc` | Global `KILO_CONFIG_DIR`, `dirname(KILO_CONFIG)`, `XDG_CONFIG_HOME/kilo`, or `~/.config/kilo`; local `./.kilo` | GSD owns generated command/agent files and GSD entries in structured config only | [Custom subagents](https://docs.kilo.ai/docs/customize/custom-subagents); docs not versioned, checked 2026-05-11 | | Gemini CLI | TOML slash commands in `commands/gsd/*.toml`; agents in `agents/gsd-*.md`; `settings.json` feature flag, hooks, and statusline | Global `GEMINI_CONFIG_DIR` or `~/.gemini`; local `./.gemini` | GSD owns generated commands/agents/hooks and only GSD settings entries; local command copy may be skipped when global GSD commands already exist | [Custom commands](https://google-gemini.github.io/gemini-cli/docs/cli/custom-commands.html), [configuration](https://google-gemini.github.io/gemini-cli/docs/cli/configuration.html); docs checked 2026-05-11 | +| Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 | | Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/`; `[agents.gsd-*]` and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, `agents.gsd-*` config sections, `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) | | GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR` or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 | | Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agent` | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. | diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index 30ac2cd26..df572ffe1 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -14,9 +14,11 @@ * cline — Rules-based; commands are embedded in .clinerules. Cline does * not use a skills/ directory. getGlobalSkillDir() returns null * for cline so the caller can emit an appropriate warning. - * kimi — Agent Skills are discovered from Kimi's generic user root: - * ~/.agents/skills. ~/.kimi-code/skills is compatible with - * Kimi Code CLI and can be selected with KIMI_CONFIG_DIR. + * kimi — Agent Skills are discovered from Kimi's generic user roots: + * ~/.config/agents/skills (recommended) then ~/.agents/skills, + * with Kimi selecting the first existing generic skills directory. + * ~/.kimi-code/skills is brand-specific and can be selected with + * KIMI_CONFIG_DIR. */ import os from 'node:os'; @@ -38,6 +40,12 @@ export interface ResolveAntigravityOpts { existsSync?: (p: string) => boolean; } +export interface ResolveKimiOpts { + env?: Record; + home?: string; + existsSync?: (p: string) => boolean; +} + /** * Resolve Antigravity global config dir across 1.x and 2.x layouts. */ @@ -61,6 +69,34 @@ export function resolveAntigravityGlobalDir(opts: ResolveAntigravityOpts = {}): return path.join(base, 'antigravity'); } +/** + * Resolve Kimi's generic user root using Kimi CLI's documented first-existing + * generic skills directory policy: + * + * 1. ~/.config/agents/skills (recommended) + * 2. ~/.agents/skills + * + * If neither generic skills directory exists yet, install to the recommended + * ~/.config/agents root so the generated skills become the first generic + * candidate Kimi discovers. + */ +export function resolveKimiGlobalDir(opts: ResolveKimiOpts = {}): string { + const env: Record = opts.env ?? process.env; + const home = opts.home ?? os.homedir(); + const existsSyncFn = opts.existsSync ?? fs.existsSync; + + if (env['KIMI_CONFIG_DIR']) return expandTilde(env['KIMI_CONFIG_DIR']); + + const recommendedRoot = path.join(home, '.config', 'agents'); + const fallbackRoot = path.join(home, '.agents'); + const candidates = [recommendedRoot, fallbackRoot]; + for (const candidate of candidates) { + if (existsSyncFn(path.join(candidate, 'skills'))) return candidate; + } + + return recommendedRoot; +} + /** * Return the global config base directory for the given runtime. * Respects the same env-var overrides as bin/install.js getGlobalDir(). @@ -130,8 +166,7 @@ export function getGlobalConfigDir(runtime: string): string { // ── Kimi CLI (generic agents user root) ──────────────────────────────── case 'kimi': { - if (env['KIMI_CONFIG_DIR']) return expandTilde(env['KIMI_CONFIG_DIR']); - return path.join(home, '.agents'); + return resolveKimiGlobalDir({ env, home }); } // ── OpenCode (XDG) ─────────────────────────────────────────────────────── diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 0d6fe1668..9e2a11a4d 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -19,6 +19,7 @@ const { getGlobalConfigDir, getGlobalSkillsBase, getGlobalSkillDir, + resolveKimiGlobalDir, } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs')); const { resolveRuntimeArtifactLayout, @@ -47,25 +48,56 @@ function withEnv(updates, fn) { } describe('Kimi runtime homes', () => { - test('canonical global skills base is ~/.agents/skills, not ~/.kimi-code/skills', () => { - withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: undefined }, () => { + test('default Kimi global root is recommended ~/.config/agents when no generic skills root exists', () => { + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-home-default-')); + try { + withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: undefined, HOME: tmpHome, USERPROFILE: tmpHome }, () => { + assert.strictEqual( + getGlobalConfigDir('kimi'), + path.join(tmpHome, '.config', 'agents'), + ); + assert.strictEqual( + getGlobalSkillsBase('kimi'), + path.join(tmpHome, '.config', 'agents', 'skills'), + ); + assert.strictEqual( + getGlobalSkillDir('kimi', 'gsd-help'), + path.join(tmpHome, '.config', 'agents', 'skills', 'gsd-help'), + ); + assert.notStrictEqual( + getGlobalSkillsBase('kimi'), + path.join(tmpHome, '.kimi-code', 'skills'), + ); + }); + } finally { + cleanup(tmpHome); + } + }); + + test('Kimi root resolution follows first-existing generic skills directory order', () => { + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-home-existing-')); + try { + const recommendedRoot = path.join(tmpHome, '.config', 'agents'); + const fallbackRoot = path.join(tmpHome, '.agents'); assert.strictEqual( - getGlobalConfigDir('kimi'), - path.join(os.homedir(), '.agents'), + resolveKimiGlobalDir({ env: {}, home: tmpHome, existsSync: fs.existsSync }), + recommendedRoot, ); + + fs.mkdirSync(path.join(fallbackRoot, 'skills'), { recursive: true }); assert.strictEqual( - getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.agents', 'skills'), + resolveKimiGlobalDir({ env: {}, home: tmpHome, existsSync: fs.existsSync }), + fallbackRoot, ); + + fs.mkdirSync(path.join(recommendedRoot, 'skills'), { recursive: true }); assert.strictEqual( - getGlobalSkillDir('kimi', 'gsd-help'), - path.join(os.homedir(), '.agents', 'skills', 'gsd-help'), + resolveKimiGlobalDir({ env: {}, home: tmpHome, existsSync: fs.existsSync }), + recommendedRoot, ); - assert.notStrictEqual( - getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.kimi-code', 'skills'), - ); - }); + } finally { + cleanup(tmpHome); + } }); test('KIMI_CONFIG_DIR can select the brand-specific ~/.kimi-code root', () => { @@ -80,16 +112,55 @@ describe('Kimi runtime homes', () => { }); test('XDG_CONFIG_HOME does not change Kimi default root', () => { - withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/xdg-home' }, () => { + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-home-xdg-')); + try { + withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/xdg-home', HOME: tmpHome, USERPROFILE: tmpHome }, () => { + assert.strictEqual( + getGlobalConfigDir('kimi'), + path.join(tmpHome, '.config', 'agents'), + ); + assert.strictEqual( + getConfigDirFromHome('kimi', true), + "'.config', 'agents'", + ); + }); + } finally { + cleanup(tmpHome); + } + }); + + test('Kimi global install reuses existing ~/.agents/skills when recommended root is absent', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-existing-agents-project-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-existing-agents-home-')); + try { + fs.mkdirSync(path.join(tmpHome, '.agents', 'skills'), { recursive: true }); + const result = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--global', '--no-sdk'], + { + cwd: tmpProject, + encoding: 'utf8', + env: installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }), + }, + ); + assert.strictEqual( - getGlobalConfigDir('kimi'), - path.join(os.homedir(), '.agents'), + result.status, + 0, + `expected --kimi --global to reuse existing ~/.agents/skills\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); assert.strictEqual( - getConfigDirFromHome('kimi', true), - "'.agents'", + fs.existsSync(path.join(tmpHome, '.agents', 'skills', 'gsd-new-project', 'SKILL.md')), + true, ); - }); + assert.strictEqual( + fs.existsSync(path.join(tmpHome, '.config', 'agents', 'skills', 'gsd-new-project', 'SKILL.md')), + false, + ); + } finally { + cleanup(tmpProject); + cleanup(tmpHome); + } }); }); diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index fb499452b..a56b2539a 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -48,7 +48,7 @@ const RUNTIME_META = { cursor: { localDir: '.cursor', globalSuffix: '.cursor' }, gemini: { localDir: '.gemini', globalSuffix: '.gemini' }, hermes: { localDir: '.hermes', globalSuffix: '.hermes' }, - kimi: { localDir: '.kimi-code', globalSuffix: '.agents' }, + kimi: { localDir: '.kimi-code', globalSuffix: path.join('.config', 'agents') }, kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 771440a52..66eaa1050 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -187,8 +187,8 @@ describe('install.js exports multi-select runtime metadata', () => { 'prompt lists Hermes Agent as option 10'); assert.ok(/\b11\)\s*Kimi\b/.test(prompt), 'prompt lists Kimi as option 11'); - assert.ok(/Kimi\s+\(~\/\.agents\)/.test(prompt), - 'prompt shows the canonical Kimi global root'); + assert.ok(/Kimi\s+\(~\/\.config\/agents, then ~\/\.agents if existing\)/.test(prompt), + 'prompt shows the Kimi first-existing generic root policy'); assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt), 'prompt lists Qwen Code as option 14'); assert.ok(/\b15\)\s*Trae\b/.test(prompt), From 0fd4d703139d407dbd4d62eb4200edad595fa54b Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:38:08 +0200 Subject: [PATCH 25/30] fix: track kimi agent stage dir immediately --- src/runtime-artifact-layout.cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index 3bda4d043..d52ced4a9 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -224,6 +224,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): const rootAgent = `---\nname: gsd\ndescription: Run GSD workflows in Kimi CLI.\ntools: Agent\n---\n\n# GSD for Kimi CLI\n\nCoordinate installed /skill:gsd-* workflows and route work to generated GSD subagents when a workflow requires an agent handoff.\n`; const artifacts = buildKimiAgentArtifacts({ rootAgent, subagents }); const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-agents-')); + installProfiles.STAGED_DIRS.add(stageDir); fs.writeFileSync(path.join(stageDir, 'gsd.yaml'), artifacts.root.yaml); fs.writeFileSync(path.join(stageDir, 'gsd.md'), artifacts.root.prompt); const subagentsDir = path.join(stageDir, 'subagents'); @@ -232,7 +233,6 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): fs.writeFileSync(path.join(subagentsDir, `${artifact.name}.yaml`), artifact.yaml); fs.writeFileSync(path.join(subagentsDir, `${artifact.name}.md`), artifact.prompt); } - installProfiles.STAGED_DIRS.add(stageDir); return stageDir; }, }; From 50ac0fcba86df4ae8811e6b3fe3f33a0a5a47342 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:41:29 +0200 Subject: [PATCH 26/30] test: cover kimi agent staging cleanup tracking --- tests/runtime-artifact-layout.test.cjs | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/runtime-artifact-layout.test.cjs b/tests/runtime-artifact-layout.test.cjs index bb1d5d541..5cc091006 100644 --- a/tests/runtime-artifact-layout.test.cjs +++ b/tests/runtime-artifact-layout.test.cjs @@ -23,6 +23,7 @@ const fs = require('fs'); const path = require('path'); const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs'); +const installProfiles = require('../gsd-core/bin/lib/install-profiles.cjs'); const FAKE_DIR = '/tmp/fake-config-dir'; @@ -444,6 +445,42 @@ describe('stage — skills kind (kimi global)', () => { assert.match(executorYaml, /kimi_cli\.tools\./); assert.doesNotMatch(executorYaml, /mcp__/); }); + + test('tracks Kimi agent staging dir before writing artifacts', () => { + const layout = resolveRuntimeArtifactLayout('kimi', FAKE_STAGE_DIR, 'global'); + const agentsKind = layout.kinds.find(k => k.kind === 'kimi-agents'); + assert.ok(agentsKind, 'should have a kimi-agents kind'); + + const originalWriteFileSync = fs.writeFileSync; + const before = new Set(installProfiles.STAGED_DIRS); + let added = []; + + try { + fs.writeFileSync = function writeFileSyncWithInjectedFailure(file, ...args) { + const filePath = String(file); + if (filePath.includes('gsd-kimi-agents-') && path.basename(filePath) === 'gsd.yaml') { + throw new Error('forced Kimi stage write failure'); + } + return originalWriteFileSync.call(this, file, ...args); + }; + + assert.throws( + () => agentsKind.stage(PROFILE_FULL), + /forced Kimi stage write failure/ + ); + + added = [...installProfiles.STAGED_DIRS] + .filter(dir => !before.has(dir) && path.basename(dir).startsWith('gsd-kimi-agents-')); + assert.strictEqual(added.length, 1, 'partially written Kimi stage dir must be tracked for cleanup'); + assert.ok(fs.existsSync(added[0]), 'tracked partial Kimi stage dir should exist'); + } finally { + fs.writeFileSync = originalWriteFileSync; + for (const dir of added) { + fs.rmSync(dir, { recursive: true, force: true }); + installProfiles.STAGED_DIRS.delete(dir); + } + } + }); }); describe('stage — opencode commands kind', () => { From cd9804305c1d7f43e4201a612bea93738a928045 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:09:09 +0200 Subject: [PATCH 27/30] docs: clarify Kimi config root discovery --- docs/how-to/install-on-your-runtime.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index d7229890b..e82b342aa 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -191,18 +191,14 @@ Then launch the generated agent from that directory: kimi --agent-file ~/.kimi-code/agents/gsd.yaml ``` -**Override the install directory:** - -```bash -KIMI_CONFIG_DIR=~/.agents-alt npx @opengsd/gsd-core@latest --kimi --global -``` - For brand-specific scripted installs, use: ```bash KIMI_CONFIG_DIR=~/.kimi-code npx @opengsd/gsd-core@latest --kimi --global ``` +Avoid arbitrary `KIMI_CONFIG_DIR` roots unless your Kimi configuration also adds the matching `skills/` directory to Kimi's extra skill directories. GSD can write files there, but Kimi will not auto-discover skills outside its documented generic and brand-specific roots without that Kimi-side configuration. + `--kimi --local` is intentionally deferred and guarded in v1; use the global install path above for Kimi CLI. --- From 7f868dcc6b2c51e8496eb56ab5d5716a2ff7ed39 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:40:18 +0200 Subject: [PATCH 28/30] fix: close Kimi runtime review gaps --- bin/install.js | 2 + docs/installer-migrations.md | 2 +- gsd-core/bin/shared/model-catalog.json | 5 ++ src/core.cts | 13 +++- src/runtime-homes.cts | 8 +- tests/agent-install-validation.test.cjs | 75 +++++++++++++++++++ .../bug-kimi-path-layout-local-guard.test.cjs | 5 +- tests/model-catalog-runtime-defaults.test.cjs | 6 ++ 8 files changed, 111 insertions(+), 5 deletions(-) diff --git a/bin/install.js b/bin/install.js index 9cdc052a9..d95c00812 100755 --- a/bin/install.js +++ b/bin/install.js @@ -9077,6 +9077,8 @@ function reportLocalPatches(configDir, runtime = 'claude') { ? '$gsd-update --reapply' : runtime === 'cursor' ? 'gsd-update --reapply (mention the skill name)' + : runtime === 'kimi' + ? '/skill:gsd-update --reapply' : '/gsd-update --reapply'; console.log(''); console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):'); diff --git a/docs/installer-migrations.md b/docs/installer-migrations.md index 43d911ffb..cfbbd169d 100644 --- a/docs/installer-migrations.md +++ b/docs/installer-migrations.md @@ -368,7 +368,7 @@ for the new shape before changing migration behavior. | OpenCode | Flat markdown commands in `command/gsd-*.md`; agents in `agents/gsd-*.md`; config updates in `opencode.json` or `opencode.jsonc` | Global `OPENCODE_CONFIG_DIR`, `dirname(OPENCODE_CONFIG)`, `XDG_CONFIG_HOME/opencode`, or `~/.config/opencode`; local `./.opencode` | GSD owns generated command/agent files and GSD entries in structured config only | [Config](https://opencode.ai/docs/config/); docs published 2026-05, checked 2026-05-11 | | Kilo | OpenCode-style flat markdown commands in `command/gsd-*.md`; agents in `agents/gsd-*.md`; config updates in `kilo.json` or `kilo.jsonc` | Global `KILO_CONFIG_DIR`, `dirname(KILO_CONFIG)`, `XDG_CONFIG_HOME/kilo`, or `~/.config/kilo`; local `./.kilo` | GSD owns generated command/agent files and GSD entries in structured config only | [Custom subagents](https://docs.kilo.ai/docs/customize/custom-subagents); docs not versioned, checked 2026-05-11 | | Gemini CLI | TOML slash commands in `commands/gsd/*.toml`; agents in `agents/gsd-*.md`; `settings.json` feature flag, hooks, and statusline | Global `GEMINI_CONFIG_DIR` or `~/.gemini`; local `./.gemini` | GSD owns generated commands/agents/hooks and only GSD settings entries; local command copy may be skipped when global GSD commands already exist | [Custom commands](https://google-gemini.github.io/gemini-cli/docs/cli/custom-commands.html), [configuration](https://google-gemini.github.io/gemini-cli/docs/cli/configuration.html); docs checked 2026-05-11 | -| Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 | +| Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; `KIMI_CONFIG_DIR` and `--config-dir` are GSD write-location overrides and arbitrary roots require Kimi-side `--skills-dir` or `extra_skill_dirs` configuration for skill discovery; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 | | Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/`; `[agents.gsd-*]` and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, `agents.gsd-*` config sections, `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) | | GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 | | Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agent` | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. | diff --git a/gsd-core/bin/shared/model-catalog.json b/gsd-core/bin/shared/model-catalog.json index 8e0d37d97..ac67efe7f 100644 --- a/gsd-core/bin/shared/model-catalog.json +++ b/gsd-core/bin/shared/model-catalog.json @@ -52,6 +52,11 @@ "sonnet": null, "haiku": null }, + "kimi": { + "opus": null, + "sonnet": null, + "haiku": null + }, "cursor": { "opus": null, "sonnet": null, diff --git a/src/core.cts b/src/core.cts index a0203206e..1c04d2bfe 100644 --- a/src/core.cts +++ b/src/core.cts @@ -1293,7 +1293,18 @@ function checkAgentsInstalled(runtime?: string): AgentsInstalledResult { const agentFile = path.join(agentsDir, `${agent}.md`); const agentFileCopilot = path.join(agentsDir, `${agent}.agent.md`); const agentFileCodex = path.join(agentsDir, `${agent}.toml`); - if (fs.existsSync(agentFile) || fs.existsSync(agentFileCopilot) || fs.existsSync(agentFileCodex)) { + const agentFileKimiYaml = path.join(agentsDir, 'subagents', `${agent}.yaml`); + const agentFileKimiPrompt = path.join(agentsDir, 'subagents', `${agent}.md`); + const kimiAgentInstalled = + resolvedRuntime === 'kimi' && + fs.existsSync(agentFileKimiYaml) && + fs.existsSync(agentFileKimiPrompt); + if ( + fs.existsSync(agentFile) || + fs.existsSync(agentFileCopilot) || + fs.existsSync(agentFileCodex) || + kimiAgentInstalled + ) { installed.push(agent); } else { missing.push(agent); diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index 18d33d75d..9eaf68637 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -17,8 +17,8 @@ * kimi — Agent Skills are discovered from Kimi's generic user roots: * ~/.config/agents/skills (recommended) then ~/.agents/skills, * with Kimi selecting the first existing generic skills directory. - * ~/.kimi-code/skills is brand-specific and can be selected with - * KIMI_CONFIG_DIR. + * ~/.kimi-code/skills is brand-specific and can be selected as a + * GSD write target with --config-dir or KIMI_CONFIG_DIR. */ import os from 'node:os'; @@ -79,6 +79,10 @@ export function resolveAntigravityGlobalDir(opts: ResolveAntigravityOpts = {}): * If neither generic skills directory exists yet, install to the recommended * ~/.config/agents root so the generated skills become the first generic * candidate Kimi discovers. + * + * KIMI_CONFIG_DIR is a GSD installer write-location override. It is not Kimi's + * upstream data-root variable, and arbitrary roots are discoverable by Kimi only + * when the user also configures Kimi --skills-dir or extra_skill_dirs. */ export function resolveKimiGlobalDir(opts: ResolveKimiOpts = {}): string { const env: Record = opts.env ?? process.env; diff --git a/tests/agent-install-validation.test.cjs b/tests/agent-install-validation.test.cjs index 17c03a858..4dd8bce6f 100644 --- a/tests/agent-install-validation.test.cjs +++ b/tests/agent-install-validation.test.cjs @@ -10,12 +10,17 @@ const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); +const os = require('os'); const path = require('path'); +const { spawnSync } = require('node:child_process'); const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); +const { installerEnv } = require('./helpers/install-shared.cjs'); const AGENTS_DIR_NAME = 'agents'; const MODEL_PROFILES = require('../gsd-core/bin/lib/model-profiles.cjs').MODEL_PROFILES; const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES); +const ROOT = path.join(__dirname, '..'); +const INSTALL_SCRIPT = path.join(ROOT, 'bin', 'install.js'); /** * Create a fake GSD install directory structure that mirrors what the installer @@ -276,6 +281,76 @@ describe('checkAgentsInstalled: Copilot .agent.md format (#1512)', () => { }); }); +// ─── Kimi agents/subagents detection (#743 review) ───────────────────────── + +describe('checkAgentsInstalled: Kimi agents/subagents layout', () => { + test('Kimi install is detected by init, validate agents, and health checks', () => { + const tmpDir = createTempProject('gsd-kimi-agent-status-project-'); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-agent-status-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-agent-status-home-')); + const env = installerEnv({ + HOME: tmpHome, + USERPROFILE: tmpHome, + KIMI_CONFIG_DIR: tmpConfig, + }); + + try { + const installResult = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk'], + { + cwd: tmpDir, + encoding: 'utf8', + env, + }, + ); + assert.strictEqual( + installResult.status, + 0, + `Kimi install failed\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`, + ); + + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ runtime: 'kimi', model_profile: 'balanced' }, null, 2), + ); + + const initResult = runGsdTools('init new-workspace --raw', tmpDir, env); + assert.ok(initResult.success, `init failed: ${initResult.error}`); + const initOutput = JSON.parse(initResult.output); + assert.strictEqual(initOutput.agent_runtime, 'kimi'); + assert.strictEqual(initOutput.agents_dir, path.join(tmpConfig, 'agents')); + assert.strictEqual(initOutput.agents_installed, true); + assert.deepStrictEqual(initOutput.missing_agents, []); + + const validateResult = runGsdTools('validate agents --raw', tmpDir, { + ...env, + GSD_RUNTIME: 'kimi', + }); + assert.ok(validateResult.success, `validate agents failed: ${validateResult.error}`); + const validateOutput = JSON.parse(validateResult.output); + assert.strictEqual(validateOutput.agents_dir, path.join(tmpConfig, 'agents')); + assert.strictEqual(validateOutput.agents_found, true); + assert.deepStrictEqual(validateOutput.missing, []); + + const healthResult = runGsdTools('validate health --raw', tmpDir, { + ...env, + GSD_RUNTIME: 'kimi', + }); + assert.ok(healthResult.success, `validate health failed: ${healthResult.error}`); + const healthOutput = JSON.parse(healthResult.output); + const agentWarnings = (healthOutput.warnings || []).filter( + (warning) => warning.code === 'W010' || /GSD agents/i.test(warning.message || ''), + ); + assert.deepStrictEqual(agentWarnings, []); + } finally { + cleanup(tmpDir); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); +}); + // ─── validate agents subcommand ───────────────────────────────────────────── describe('validate agents subcommand (#1371)', () => { diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 9e2a11a4d..db97faa77 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -334,7 +334,10 @@ describe('Kimi local install guard', () => { 0, `second install failed\nstdout: ${second.stdout}\nstderr: ${second.stderr}`, ); - assert.match(`${second.stdout}\n${second.stderr}`, /locally modified GSD file/i); + const secondOutput = `${second.stdout}\n${second.stderr}`; + assert.match(secondOutput, /locally modified GSD file/i); + assert.match(secondOutput, /\/skill:gsd-update --reapply/); + assert.doesNotMatch(secondOutput, /Run\s+\/gsd-update --reapply/); const skillBackup = path.join(tmpConfig, 'gsd-local-patches', 'skills', 'gsd-new-project', 'SKILL.md'); const agentBackup = path.join(tmpConfig, 'gsd-local-patches', 'agents', 'subagents', 'gsd-executor.md'); diff --git a/tests/model-catalog-runtime-defaults.test.cjs b/tests/model-catalog-runtime-defaults.test.cjs index f071ed6f8..a619bb98b 100644 --- a/tests/model-catalog-runtime-defaults.test.cjs +++ b/tests/model-catalog-runtime-defaults.test.cjs @@ -9,6 +9,7 @@ const fs = require('node:fs'); const path = require('node:path'); const { catalog, KNOWN_RUNTIMES } = require('../gsd-core/bin/lib/model-catalog.cjs'); +const { allRuntimes } = require('../bin/install.js'); const ROOT = path.join(__dirname, '..'); const SETTINGS_ADVANCED = fs.readFileSync(path.join(ROOT, 'gsd-core', 'workflows', 'settings-advanced.md'), 'utf8'); @@ -17,9 +18,14 @@ const CONFIG_DOC = fs.readFileSync(path.join(ROOT, 'docs', 'CONFIGURATION.md'), describe('model catalog runtime defaults parity (#3229)', () => { test('known runtimes include hermes and match catalog keys', () => { assert.ok(KNOWN_RUNTIMES.has('hermes')); + assert.ok(KNOWN_RUNTIMES.has('kimi')); assert.deepStrictEqual([...KNOWN_RUNTIMES].sort(), Object.keys(catalog.runtimeTierDefaults).sort()); }); + test('installer-supported runtimes are all known to the model catalog', () => { + assert.deepStrictEqual([...allRuntimes].sort(), [...KNOWN_RUNTIMES].sort()); + }); + test('settings-advanced runtime defaults table matches catalog for concrete runtimes', () => { for (const [runtime, tiers] of Object.entries(catalog.runtimeTierDefaults)) { if (!tiers.opus) continue; // Group B runtimes intentionally have no built-ins From 69f6efebf19834ac6dc3a0b40279c5eecf6a2cfd Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:30:21 +0200 Subject: [PATCH 29/30] =?UTF-8?q?docs(#743):=20address=20Kimi=20review=20?= =?UTF-8?q?=E2=80=94=20runtime=20count=20+=20support=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address remaining review feedback on PR #743: - Bump supported-runtime count 15 -> 16 across user-facing READMEs (docs/README.md + ja-JP/zh-CN translations) now that Kimi is added. - Add an explicit support-boundary note in the Kimi CLI install section: the kimi --agent-file custom-agent contract targets legacy/Python kimi-cli; newer npm Kimi Code (@moonshot-ai/kimi-code) rejects --agent-file and consumes the same /skill:gsd-* skills via --skills-dir. - Mirror the boundary in the changeset so release notes are unambiguous. Historical ADR/changeset/INVENTORY references to '15 runtimes' are left intact as point-in-time records. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/kimi-runtime-support.md | 2 +- docs/README.md | 2 +- docs/how-to/install-on-your-runtime.md | 2 ++ docs/ja-JP/README.md | 2 +- docs/zh-CN/README.md | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/kimi-runtime-support.md b/.changeset/kimi-runtime-support.md index 2aa33a6e7..364b350b9 100644 --- a/.changeset/kimi-runtime-support.md +++ b/.changeset/kimi-runtime-support.md @@ -2,4 +2,4 @@ type: Added pr: 743 --- -**Kimi CLI runtime support is now documented and installable** — users can install global GSD Agent Skills with `--kimi --global`, invoke them as `/skill:gsd-*`, and launch the generated custom agent explicitly with `kimi --agent-file`. +**Kimi CLI runtime support is now documented and installable** — users can install global GSD Agent Skills with `--kimi --global`, invoke them as `/skill:gsd-*`, and launch the generated custom agent explicitly with `kimi --agent-file`. The custom-agent (`--agent-file`) surface targets the legacy/Python `kimi-cli` contract; newer Kimi Code (`@moonshot-ai/kimi-code`) consumes the same `/skill:gsd-*` skills via `--skills-dir` instead. diff --git a/docs/README.md b/docs/README.md index 5d773af02..aebdd7d81 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) ## How-to guides -- [Install on your runtime](how-to/install-on-your-runtime.md) — runtime-specific install steps for all 15 supported runtimes +- [Install on your runtime](how-to/install-on-your-runtime.md) — runtime-specific install steps for all 16 supported runtimes - [Install a minimal GSD and add skills later](how-to/install-minimal-and-add-skills.md) — install only the core skills, then grow the surface with profiles and `/gsd:surface` - [Discuss a phase](how-to/discuss-a-phase.md) — capture implementation decisions before planning begins - [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 5e7b5d90b..6a89f9c1c 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -214,6 +214,8 @@ All registered hooks are managed by GSD and are removed cleanly on `--uninstall` ### Kimi CLI +> **Support boundary — legacy `kimi-cli` vs Kimi Code.** This integration targets the legacy/Python `kimi-cli` custom-agent contract. The `kimi --agent-file /agents/gsd.yaml` launch shown below is accepted by `kimi-cli`. The newer npm Kimi Code (`@moonshot-ai/kimi-code`, e.g. `0.11.0`) does **not** accept `--agent-file`; it discovers skills through fixed skill roots and `--skills-dir`. The generated `/skill:gsd-*` skills work in both, but the custom-agent (`--agent-file`) surface is specific to legacy `kimi-cli`. For Kimi Code, point it at the installed skills root with `--skills-dir /skills` instead of using `--agent-file`. + ```bash npx @opengsd/gsd-core@latest --kimi --global ``` diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md index 96a7174da..363ae0727 100644 --- a/docs/ja-JP/README.md +++ b/docs/ja-JP/README.md @@ -15,7 +15,7 @@ ## How-to guides -- [ランタイムへのインストール](how-to/install-on-your-runtime.md) — サポートされる全 15 ランタイムのランタイム別インストール手順 +- [ランタイムへのインストール](how-to/install-on-your-runtime.md) — サポートされる全 16 ランタイムのランタイム別インストール手順 - [フェーズを議論する](how-to/discuss-a-phase.md) — 計画を始める前に実装上の決定事項を記録する - [フェーズを計画する](how-to/plan-a-phase.md) — リサーチを実行し、作業を分解し、計画の品質を検証する - [フェーズを実行する](how-to/execute-a-phase.md) — 新鮮なコンテキストのサブエージェントで並列ウェーブとして計画を実行する diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 2132cb38f..cde3e49b7 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -15,7 +15,7 @@ ## How-to guides -- [在你的运行时上安装](how-to/install-on-your-runtime.md) — 适用于全部 15 个受支持运行时的安装步骤 +- [在你的运行时上安装](how-to/install-on-your-runtime.md) — 适用于全部 16 个受支持运行时的安装步骤 - [讨论一个阶段](how-to/discuss-a-phase.md) — 在规划开始前记录实现决策 - [规划一个阶段](how-to/plan-a-phase.md) — 执行调研、分解工作并验证计划质量 - [执行一个阶段](how-to/execute-a-phase.md) — 使用全新上下文的子代理以并行波次运行计划 From 560b59240fd7676db50b67c1999b25f488b5c3f0 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 8 Jun 2026 23:07:48 -0400 Subject: [PATCH 30/30] fix(#743): resolve Kimi branch lint failures --- bin/install.js | 4 ---- tests/runtime-artifact-layout.test.cjs | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/install.js b/bin/install.js index 249d3eece..37e26aebd 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2311,10 +2311,6 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c return `${fm}\n${normalizedBody}`; } -function escapeRegExp(value) { - return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - function normalizeKimiSkillName(skillName) { let text = String(skillName || '').trim().toLowerCase(); if (text.startsWith('/')) text = text.slice(1); diff --git a/tests/runtime-artifact-layout.test.cjs b/tests/runtime-artifact-layout.test.cjs index 9e7d2b75f..786f7283f 100644 --- a/tests/runtime-artifact-layout.test.cjs +++ b/tests/runtime-artifact-layout.test.cjs @@ -24,6 +24,7 @@ const path = require('path'); const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs'); const installProfiles = require('../gsd-core/bin/lib/install-profiles.cjs'); +const { cleanup } = require('./helpers.cjs'); const FAKE_DIR = '/tmp/fake-config-dir'; @@ -551,7 +552,7 @@ describe('stage — skills kind (kimi global)', () => { } finally { fs.writeFileSync = originalWriteFileSync; for (const dir of added) { - fs.rmSync(dir, { recursive: true, force: true }); + cleanup(dir); installProfiles.STAGED_DIRS.delete(dir); } }