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