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] 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