@opengsd/gsd-core 1.4.3 → 1.5.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-executor.md +1 -1
  4. package/agents/gsd-plan-checker.md +11 -2
  5. package/agents/gsd-planner.md +11 -7
  6. package/agents/gsd-verifier.md +3 -3
  7. package/bin/install.js +825 -149
  8. package/commands/gsd/autonomous.md +5 -1
  9. package/commands/gsd/ns-manage.md +8 -1
  10. package/commands/gsd/ns-project.md +5 -0
  11. package/commands/gsd/ns-review.md +4 -1
  12. package/commands/gsd/ns-workflow.md +7 -1
  13. package/commands/gsd/plan-review-convergence.md +5 -4
  14. package/commands/gsd/surface.md +12 -5
  15. package/gemini-extension.json +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +198 -101
  17. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +417 -0
  19. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  20. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  21. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  22. package/gsd-core/bin/lib/configuration.cjs +4 -34
  23. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  24. package/gsd-core/bin/lib/core.cjs +57 -1817
  25. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  26. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  27. package/gsd-core/bin/lib/init.cjs +58 -12
  28. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  29. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  30. package/gsd-core/bin/lib/io.cjs +167 -0
  31. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  32. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  33. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  34. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  35. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  36. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  37. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  38. package/gsd-core/bin/lib/roadmap.cjs +4 -1
  39. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
  40. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
  41. package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
  42. package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
  43. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  44. package/gsd-core/bin/lib/state.cjs +114 -2
  45. package/gsd-core/bin/lib/surface.cjs +66 -14
  46. package/gsd-core/bin/lib/verify.cjs +1 -1
  47. package/gsd-core/bin/shared/model-catalog.json +5 -0
  48. package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
  49. package/gsd-core/references/planner-reviews.md +5 -2
  50. package/gsd-core/templates/phase-prompt.md +7 -7
  51. package/gsd-core/templates/project.md +19 -2
  52. package/gsd-core/templates/summary-complex.md +1 -0
  53. package/gsd-core/templates/summary-minimal.md +1 -0
  54. package/gsd-core/templates/summary-standard.md +1 -0
  55. package/gsd-core/templates/summary.md +1 -0
  56. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  57. package/gsd-core/workflows/add-backlog.md +1 -1
  58. package/gsd-core/workflows/add-phase.md +1 -1
  59. package/gsd-core/workflows/add-tests.md +1 -1
  60. package/gsd-core/workflows/add-todo.md +1 -1
  61. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  62. package/gsd-core/workflows/audit-fix.md +1 -1
  63. package/gsd-core/workflows/audit-milestone.md +1 -1
  64. package/gsd-core/workflows/audit-uat.md +1 -1
  65. package/gsd-core/workflows/autonomous.md +86 -15
  66. package/gsd-core/workflows/check-todos.md +1 -1
  67. package/gsd-core/workflows/cleanup.md +1 -1
  68. package/gsd-core/workflows/code-review-fix.md +6 -4
  69. package/gsd-core/workflows/code-review.md +3 -2
  70. package/gsd-core/workflows/complete-milestone.md +11 -5
  71. package/gsd-core/workflows/debug.md +1 -1
  72. package/gsd-core/workflows/diagnose-issues.md +1 -1
  73. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  74. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  75. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  76. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  77. package/gsd-core/workflows/discuss-phase.md +8 -1
  78. package/gsd-core/workflows/do.md +1 -1
  79. package/gsd-core/workflows/docs-update.md +1 -1
  80. package/gsd-core/workflows/edit-phase.md +1 -1
  81. package/gsd-core/workflows/eval-review.md +4 -1
  82. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  83. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  84. package/gsd-core/workflows/execute-phase.md +8 -1
  85. package/gsd-core/workflows/execute-plan.md +1 -1
  86. package/gsd-core/workflows/explore.md +1 -1
  87. package/gsd-core/workflows/extract-learnings.md +1 -1
  88. package/gsd-core/workflows/forensics.md +1 -1
  89. package/gsd-core/workflows/graduation.md +1 -1
  90. package/gsd-core/workflows/health.md +1 -1
  91. package/gsd-core/workflows/help/modes/full.md +1 -1
  92. package/gsd-core/workflows/import.md +1 -1
  93. package/gsd-core/workflows/ingest-docs.md +1 -1
  94. package/gsd-core/workflows/insert-phase.md +1 -1
  95. package/gsd-core/workflows/list-workspaces.md +1 -1
  96. package/gsd-core/workflows/manager.md +1 -1
  97. package/gsd-core/workflows/map-codebase.md +1 -1
  98. package/gsd-core/workflows/milestone-summary.md +1 -1
  99. package/gsd-core/workflows/mvp-phase.md +1 -1
  100. package/gsd-core/workflows/new-milestone.md +1 -1
  101. package/gsd-core/workflows/new-project.md +1 -1
  102. package/gsd-core/workflows/new-workspace.md +1 -1
  103. package/gsd-core/workflows/next.md +1 -1
  104. package/gsd-core/workflows/pause-work.md +1 -1
  105. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  106. package/gsd-core/workflows/plan-phase.md +27 -2
  107. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  108. package/gsd-core/workflows/plant-seed.md +1 -1
  109. package/gsd-core/workflows/profile-user.md +1 -1
  110. package/gsd-core/workflows/progress.md +1 -1
  111. package/gsd-core/workflows/quick.md +2 -2
  112. package/gsd-core/workflows/remove-phase.md +1 -1
  113. package/gsd-core/workflows/remove-workspace.md +1 -1
  114. package/gsd-core/workflows/resume-project.md +1 -1
  115. package/gsd-core/workflows/review.md +1 -1
  116. package/gsd-core/workflows/scan.md +1 -1
  117. package/gsd-core/workflows/secure-phase.md +1 -1
  118. package/gsd-core/workflows/settings-advanced.md +1 -1
  119. package/gsd-core/workflows/settings-integrations.md +1 -1
  120. package/gsd-core/workflows/settings.md +1 -1
  121. package/gsd-core/workflows/ship.md +8 -1
  122. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  123. package/gsd-core/workflows/sketch.md +1 -1
  124. package/gsd-core/workflows/spec-phase.md +1 -1
  125. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  126. package/gsd-core/workflows/spike.md +1 -1
  127. package/gsd-core/workflows/stats.md +1 -1
  128. package/gsd-core/workflows/thread.md +1 -1
  129. package/gsd-core/workflows/transition.md +1 -1
  130. package/gsd-core/workflows/ui-phase.md +1 -1
  131. package/gsd-core/workflows/ui-review.md +1 -1
  132. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  133. package/gsd-core/workflows/validate-phase.md +1 -1
  134. package/gsd-core/workflows/verify-phase.md +1 -1
  135. package/gsd-core/workflows/verify-work.md +8 -1
  136. package/package.json +7 -2
  137. package/scripts/base64-scan.sh +1 -1
  138. package/scripts/changeset/cli.cjs +8 -1
  139. package/scripts/changeset/lint.cjs +38 -2
  140. package/scripts/ci-test-scope.cjs +21 -10
  141. package/scripts/gen-capability-registry.cjs +1808 -0
  142. package/scripts/gen-loop-host-contract.cjs +471 -0
  143. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  144. package/scripts/lint-regression-test-names.cjs +117 -0
  145. package/scripts/lint-test-file-count.allowlist.json +12 -4
  146. package/scripts/prompt-injection-scan.sh +4 -4
  147. package/scripts/run-tests.cjs +79 -16
  148. package/scripts/secret-scan.sh +3 -3
  149. package/scripts/run-cross-platform-tests.cjs +0 -67
package/bin/install.js CHANGED
@@ -31,12 +31,19 @@ const {
31
31
  const {
32
32
  resolveAntigravityGlobalDir,
33
33
  getGlobalConfigDir,
34
+ getGlobalSkillsBase,
34
35
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
35
36
  const {
36
37
  applyWorktreeBaseRef,
37
38
  readBaseRefFromSettings,
38
39
  } = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
39
40
  const { resolveRuntimeConfigIntent } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
41
+ // Canonical set of hook files shipped to users. Imported here so writeManifest()
42
+ // records exactly the same set that build-hooks.js copies to hooks/dist/, making
43
+ // the manifest and the installed hooks/ dir structurally identical. Avoids the
44
+ // prefix/extension-regex approach that missed managed-hooks-registry.cjs (#941).
45
+ const { HOOKS_TO_COPY: _HOOKS_TO_COPY } = require('../scripts/build-hooks.js');
46
+ const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY);
40
47
 
41
48
  /**
42
49
  * Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies
@@ -323,6 +330,13 @@ const {
323
330
  stageAgentsForProfile,
324
331
  stageSkillsForRuntimeAsSkills,
325
332
  } = require(path.join(_gsdLibDir, 'install-profiles.cjs'));
333
+ // ADR-857 phase 4c: load capability registry (optional; missing → falls back to undefined)
334
+ let _capabilityRegistry;
335
+ try {
336
+ _capabilityRegistry = require(path.join(_gsdLibDir, 'capability-registry.cjs'));
337
+ } catch (_) {
338
+ _capabilityRegistry = undefined;
339
+ }
326
340
  const {
327
341
  applyInstallerMigrationPlan,
328
342
  discoverInstallerMigrations,
@@ -348,23 +362,6 @@ const {
348
362
  const args = process.argv.slice(2);
349
363
  const hasGlobal = args.includes('--global') || args.includes('-g');
350
364
  const hasLocal = args.includes('--local') || args.includes('-l');
351
- const hasOpencode = args.includes('--opencode');
352
- const hasClaude = args.includes('--claude');
353
- const hasGemini = args.includes('--gemini');
354
- const hasKilo = args.includes('--kilo');
355
- const hasCodex = args.includes('--codex');
356
- const hasCopilot = args.includes('--copilot');
357
- const hasAntigravity = args.includes('--antigravity');
358
- const hasCursor = args.includes('--cursor');
359
- const hasWindsurf = args.includes('--windsurf');
360
- const hasAugment = args.includes('--augment');
361
- const hasTrae = args.includes('--trae');
362
- const hasQwen = args.includes('--qwen');
363
- const hasHermes = args.includes('--hermes');
364
- const hasCodebuddy = args.includes('--codebuddy');
365
- const hasCline = args.includes('--cline');
366
- const hasBoth = args.includes('--both'); // Legacy flag, keeps working
367
- const hasAll = args.includes('--all');
368
365
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
369
366
  const hasSkillsRoot = args.includes('--skills-root');
370
367
  const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
@@ -391,30 +388,37 @@ if (hasMinimal && _profileArgRaw) {
391
388
  process.exit(1);
392
389
  }
393
390
 
394
- // Runtime selection - can be set by flags or interactive prompt
395
- let selectedRuntimes = [];
396
- if (hasAll) {
397
- selectedRuntimes = ['claude', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline'];
398
- } else if (hasBoth) {
399
- selectedRuntimes = ['claude', 'opencode'];
400
- } else {
401
- if (hasClaude) selectedRuntimes.push('claude');
402
- if (hasOpencode) selectedRuntimes.push('opencode');
403
- if (hasGemini) selectedRuntimes.push('gemini');
404
- if (hasKilo) selectedRuntimes.push('kilo');
405
- if (hasCodex) selectedRuntimes.push('codex');
406
- if (hasCopilot) selectedRuntimes.push('copilot');
407
- if (hasAntigravity) selectedRuntimes.push('antigravity');
408
- if (hasCursor) selectedRuntimes.push('cursor');
409
- if (hasWindsurf) selectedRuntimes.push('windsurf');
410
- if (hasAugment) selectedRuntimes.push('augment');
411
- if (hasTrae) selectedRuntimes.push('trae');
412
- if (hasQwen) selectedRuntimes.push('qwen');
413
- if (hasHermes) selectedRuntimes.push('hermes');
414
- if (hasCodebuddy) selectedRuntimes.push('codebuddy');
415
- if (hasCline) selectedRuntimes.push('cline');
391
+ function selectRuntimesFromArgs(runtimeArgs) {
392
+ if (runtimeArgs.includes('--all')) {
393
+ return ['claude', 'kimi', 'kilo', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline'];
394
+ }
395
+ if (runtimeArgs.includes('--both')) {
396
+ return ['claude', 'opencode'];
397
+ }
398
+
399
+ const selected = [];
400
+ if (runtimeArgs.includes('--claude')) selected.push('claude');
401
+ if (runtimeArgs.includes('--opencode')) selected.push('opencode');
402
+ if (runtimeArgs.includes('--gemini')) selected.push('gemini');
403
+ if (runtimeArgs.includes('--kilo')) selected.push('kilo');
404
+ if (runtimeArgs.includes('--codex')) selected.push('codex');
405
+ if (runtimeArgs.includes('--copilot')) selected.push('copilot');
406
+ if (runtimeArgs.includes('--antigravity')) selected.push('antigravity');
407
+ if (runtimeArgs.includes('--cursor')) selected.push('cursor');
408
+ if (runtimeArgs.includes('--windsurf')) selected.push('windsurf');
409
+ if (runtimeArgs.includes('--augment')) selected.push('augment');
410
+ if (runtimeArgs.includes('--trae')) selected.push('trae');
411
+ if (runtimeArgs.includes('--qwen')) selected.push('qwen');
412
+ if (runtimeArgs.includes('--hermes')) selected.push('hermes');
413
+ if (runtimeArgs.includes('--kimi')) selected.push('kimi');
414
+ if (runtimeArgs.includes('--codebuddy')) selected.push('codebuddy');
415
+ if (runtimeArgs.includes('--cline')) selected.push('cline');
416
+ return selected;
416
417
  }
417
418
 
419
+ // Runtime selection - can be set by flags or interactive prompt
420
+ let selectedRuntimes = selectRuntimesFromArgs(args);
421
+
418
422
  // WSL + Windows Node.js detection
419
423
  // When Windows-native Node runs on WSL, os.homedir() and path.join() produce
420
424
  // backslash paths that don't resolve correctly on the Linux filesystem.
@@ -463,6 +467,7 @@ function getDirName(runtime) {
463
467
  if (runtime === 'trae') return '.trae';
464
468
  if (runtime === 'qwen') return '.qwen';
465
469
  if (runtime === 'hermes') return '.hermes';
470
+ if (runtime === 'kimi') return '.kimi-code';
466
471
  if (runtime === 'codebuddy') return '.codebuddy';
467
472
  if (runtime === 'cline') return '.cline';
468
473
  return '.claude';
@@ -509,9 +514,17 @@ function getConfigDirFromHome(runtime, isGlobal) {
509
514
  if (runtime === 'hermes') return "'.hermes'";
510
515
  if (runtime === 'codebuddy') return "'.codebuddy'";
511
516
  if (runtime === 'cline') return "'.cline'";
517
+ if (runtime === 'kimi') return "'.config', 'agents'";
512
518
  return "'.claude'";
513
519
  }
514
520
 
521
+ /**
522
+ * Compatibility seam for tests and older installer consumers.
523
+ * Runtime home resolution now lives in runtime-homes.cjs.
524
+ */
525
+ function getGlobalDir(runtime, explicitDir = null) {
526
+ return getGlobalConfigDir(runtime, explicitDir);
527
+ }
515
528
  const banner = '\n' +
516
529
  cyan + ' ██████╗ ███████╗██████╗\n' +
517
530
  ' ██╔════╝ ██╔════╝██╔══██╗\n' +
@@ -523,7 +536,7 @@ const banner = '\n' +
523
536
  ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' +
524
537
  ' Git. Ship. Done.\n' +
525
538
  ' A meta-prompting, context engineering and spec-driven\n' +
526
- ' development workflows for Claude Code, OpenCode, Gemini, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline and CodeBuddy.\n';
539
+ ' 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';
527
540
 
528
541
  // Pure seam: parse --config-dir / -c from an arbitrary args array.
529
542
  // Returns the path string, '' for an empty equals-form value, or null when the
@@ -580,7 +593,7 @@ if (hasUninstall) {
580
593
 
581
594
  // Show help if requested
582
595
  if (hasHelp) {
583
- 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 <path>${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=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all 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 globally${reset}\n npx ${pkg.name} --cline --global\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 / COPILOT_HOME / 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`);
596
+ 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 <path>${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=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all 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 globally${reset}\n npx ${pkg.name} --cline --global\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 / COPILOT_HOME / 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`);
584
597
  process.exit(0);
585
598
  }
586
599
 
@@ -624,8 +637,37 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
624
637
  *
625
638
  * Non-Homebrew installs (NVM, system node, Windows, etc.) are returned as-is.
626
639
  */
627
- function normalizeNodePath(execPath) {
640
+ function normalizeNodePath(execPath, opts) {
628
641
  if (!execPath) return execPath;
642
+ const env = (opts && opts.env) || process.env;
643
+ const existsSync = (opts && opts.existsSync) || fs.existsSync;
644
+
645
+ // fnm multishell shim: C:/Users/<u>/AppData/Local/fnm_multishells/<pid>_<ts>/node.exe
646
+ // These are per-shell-session ephemeral directories that fnm cleans up on shell exit.
647
+ // Probe the stable fnm alias paths instead so baked hook commands survive shell restarts.
648
+ // (#977)
649
+ // TODO: Volta (~/.volta/bin/node → ~/.volta/tools/image/node/<ver>/bin/node) and
650
+ // nvm-windows (AppData/Roaming/nvm/<ver>/node.exe) have analogous issues — future work.
651
+ const normalizedForMatch = execPath.replace(/\\/g, '/');
652
+ if (/\/fnm_multishells\/[0-9]+_[0-9]+\/node(\.exe)?$/i.test(normalizedForMatch)) {
653
+ const candidates = [];
654
+ if (env.FNM_DIR) {
655
+ // Preferred: alias installed directly under FNM_DIR/aliases/default/
656
+ candidates.push(`${env.FNM_DIR}/aliases/default/node.exe`);
657
+ // POSIX layout (no .exe)
658
+ candidates.push(`${env.FNM_DIR}/aliases/default/bin/node`);
659
+ }
660
+ if (env.APPDATA) {
661
+ // Fallback: fnm default location on Windows when FNM_DIR is not set
662
+ candidates.push(`${env.APPDATA}/fnm/aliases/default/node.exe`);
663
+ }
664
+ for (const candidate of candidates) {
665
+ if (existsSync(candidate)) return candidate;
666
+ }
667
+ // No stable alias found — return raw path unchanged (graceful fallback)
668
+ return execPath;
669
+ }
670
+
629
671
  // Intel Homebrew: /usr/local/Cellar/node/<version>/bin/node
630
672
  // or /usr/local/Cellar/node@20/<version>/bin/node
631
673
  if (/^\/usr\/local\/Cellar\/node(@\d+)?\/[^/]+\/bin\/node(\.exe)?$/.test(execPath)) {
@@ -654,11 +696,16 @@ function normalizeNodePath(execPath) {
654
696
  *
655
697
  * When `process.execPath` is a versioned Homebrew Cellar path, the stable
656
698
  * Homebrew symlink is returned instead to survive `brew upgrade node` (#3181).
699
+ *
700
+ * When `process.execPath` is an ephemeral fnm multishell shim, the stable fnm
701
+ * alias path is returned instead so managed hook commands survive shell restarts
702
+ * (#977). An optional `opts` bag (`{ env, existsSync }`) is accepted for
703
+ * testability; production callers omit it to get real env / fs.
657
704
  */
658
- function resolveNodeRunner() {
705
+ function resolveNodeRunner(opts) {
659
706
  const execPath = typeof process.execPath === 'string' ? process.execPath : '';
660
707
  if (!execPath) return null;
661
- const stablePath = normalizeNodePath(execPath);
708
+ const stablePath = normalizeNodePath(execPath, opts);
662
709
  // JSON.stringify produces a properly escaped double-quoted shell token,
663
710
  // safe for paths containing spaces or unusual characters.
664
711
  return JSON.stringify(stablePath.replace(/\\/g, '/'));
@@ -712,6 +759,10 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
712
759
  if (!entry || !Array.isArray(entry.hooks)) continue;
713
760
  for (const h of entry.hooks) {
714
761
  if (!h || typeof h.command !== 'string') continue;
762
+ // args-form entries have the script path in h.args[] and h.command is
763
+ // the launcher executable (not a managed hook command). These are
764
+ // intentional user wrappers — do not rewrite them. (#976)
765
+ if (Array.isArray(h.args) && h.args.length > 0) continue;
715
766
  let trimmed = h.command.trim();
716
767
  const hadPowerShellCallOperator = platform === 'win32' && /^&\s+/.test(trimmed);
717
768
  if (hadPowerShellCallOperator) {
@@ -1896,6 +1947,35 @@ const claudeToGeminiTools = {
1896
1947
  TodoWrite: 'write_todos',
1897
1948
  };
1898
1949
 
1950
+ // Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
1951
+ // Kimi custom agent YAML requires fully-qualified module paths.
1952
+ const claudeToKimiTools = {
1953
+ Read: 'kimi_cli.tools.file:ReadFile',
1954
+ ReadFile: 'kimi_cli.tools.file:ReadFile',
1955
+ Write: 'kimi_cli.tools.file:WriteFile',
1956
+ WriteFile: 'kimi_cli.tools.file:WriteFile',
1957
+ Edit: 'kimi_cli.tools.file:StrReplaceFile',
1958
+ MultiEdit: 'kimi_cli.tools.file:StrReplaceFile',
1959
+ StrReplaceFile: 'kimi_cli.tools.file:StrReplaceFile',
1960
+ Bash: 'kimi_cli.tools.shell:Shell',
1961
+ Shell: 'kimi_cli.tools.shell:Shell',
1962
+ Grep: 'kimi_cli.tools.file:Grep',
1963
+ Glob: 'kimi_cli.tools.file:Glob',
1964
+ Agent: 'kimi_cli.tools.agent:Agent',
1965
+ Task: 'kimi_cli.tools.agent:Agent',
1966
+ AskUserQuestion: 'kimi_cli.tools.ask_user:AskUserQuestion',
1967
+ TodoWrite: 'kimi_cli.tools.todo:SetTodoList',
1968
+ SetTodoList: 'kimi_cli.tools.todo:SetTodoList',
1969
+ WebSearch: 'kimi_cli.tools.web:SearchWeb',
1970
+ SearchWeb: 'kimi_cli.tools.web:SearchWeb',
1971
+ WebFetch: 'kimi_cli.tools.web:FetchURL',
1972
+ FetchURL: 'kimi_cli.tools.web:FetchURL',
1973
+ ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile',
1974
+ TaskList: 'kimi_cli.tools.background:TaskList',
1975
+ TaskOutput: 'kimi_cli.tools.background:TaskOutput',
1976
+ TaskStop: 'kimi_cli.tools.background:TaskStop',
1977
+ };
1978
+
1899
1979
  /**
1900
1980
  * Convert a Claude Code tool name to OpenCode format
1901
1981
  * - Applies special mappings (AskUserQuestion -> question, etc.)
@@ -1945,6 +2025,63 @@ function convertGeminiToolName(claudeTool) {
1945
2025
  return claudeTool.toLowerCase();
1946
2026
  }
1947
2027
 
2028
+ function createKimiToolDiagnostic(reason, tool, source = null) {
2029
+ const isMcp = reason === 'mcp_managed';
2030
+ return {
2031
+ level: 'warning',
2032
+ code: isMcp ? 'kimi_mcp_tool_excluded' : 'kimi_unsupported_tool',
2033
+ reason,
2034
+ message: isMcp
2035
+ ? `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.`
2036
+ : `Tool '${tool}' is not supported by the Kimi tool mapper.`,
2037
+ value: tool,
2038
+ source,
2039
+ };
2040
+ }
2041
+
2042
+ /**
2043
+ * Convert a Claude/GSD tool name to a Kimi CLI module path.
2044
+ * @returns {string|null} Kimi module path, or null when excluded/unsupported.
2045
+ */
2046
+ function convertKimiToolName(claudeTool) {
2047
+ const tool = String(claudeTool || '').trim();
2048
+ if (!tool) return null;
2049
+ if (tool.startsWith('mcp__')) return null;
2050
+ return claudeToKimiTools[tool] || null;
2051
+ }
2052
+
2053
+ function mapClaudeToolsToKimiTools(claudeTools, options = {}) {
2054
+ const diagnostics = [];
2055
+ const tools = [];
2056
+ const seen = new Set();
2057
+ const source = options && Object.prototype.hasOwnProperty.call(options, 'source')
2058
+ ? options.source
2059
+ : null;
2060
+
2061
+ for (const rawTool of Array.isArray(claudeTools) ? claudeTools : []) {
2062
+ const tool = String(rawTool || '').trim();
2063
+ if (!tool) continue;
2064
+
2065
+ if (tool.startsWith('mcp__')) {
2066
+ diagnostics.push(createKimiToolDiagnostic('mcp_managed', tool, source));
2067
+ continue;
2068
+ }
2069
+
2070
+ const kimiTool = convertKimiToolName(tool);
2071
+ if (!kimiTool) {
2072
+ diagnostics.push(createKimiToolDiagnostic('unsupported_tool', tool, source));
2073
+ continue;
2074
+ }
2075
+
2076
+ if (!seen.has(kimiTool)) {
2077
+ seen.add(kimiTool);
2078
+ tools.push(kimiTool);
2079
+ }
2080
+ }
2081
+
2082
+ return { tools, diagnostics };
2083
+ }
2084
+
1948
2085
  const claudeToKiloAgentPermissions = {
1949
2086
  Read: 'read',
1950
2087
  Write: 'edit',
@@ -2225,6 +2362,283 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
2225
2362
  return `${fm}\n${normalizedBody}`;
2226
2363
  }
2227
2364
 
2365
+ function normalizeKimiSkillName(skillName) {
2366
+ let text = String(skillName || '').trim().toLowerCase();
2367
+ if (text.startsWith('/')) text = text.slice(1);
2368
+ if (text.startsWith('$')) text = text.slice(1);
2369
+ text = text.replace(/^gsd:/, 'gsd-');
2370
+ if (!text.startsWith('gsd-')) text = `gsd-${text}`;
2371
+ text = text.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2372
+ return text || 'gsd-command';
2373
+ }
2374
+
2375
+ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
2376
+ if (!Array.isArray(cmdNames) || cmdNames.length === 0) return content;
2377
+ const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp);
2378
+ const commandGroup = commands.join('|');
2379
+ const colonPattern = new RegExp(`(?<![A-Za-z0-9_/:.-])/?gsd:(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
2380
+ const hyphenPattern = new RegExp(`(?:/|\\$)gsd-(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
2381
+
2382
+ return content
2383
+ .replace(colonPattern, (_, cmd) => `/skill:gsd-${cmd}`)
2384
+ .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
2385
+ }
2386
+
2387
+ function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
2388
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
2389
+ const kimiSkillName = normalizeKimiSkillName(skillName);
2390
+ const names = cmdNames || readGsdCommandNames();
2391
+ const description = frontmatter
2392
+ ? extractFrontmatterField(frontmatter, 'description') || `Run GSD workflow ${kimiSkillName}.`
2393
+ : `Run GSD workflow ${kimiSkillName}.`;
2394
+ const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations(
2395
+ frontmatter ? body : content,
2396
+ names
2397
+ );
2398
+
2399
+ return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`;
2400
+ }
2401
+
2402
+ const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/;
2403
+
2404
+ function parseKimiAgentSource(source) {
2405
+ if (typeof source === 'string') {
2406
+ return {
2407
+ path: null,
2408
+ content: source,
2409
+ };
2410
+ }
2411
+ if (!source || typeof source !== 'object' || typeof source.content !== 'string') {
2412
+ return null;
2413
+ }
2414
+ return {
2415
+ path: typeof source.path === 'string' ? source.path : null,
2416
+ content: source.content,
2417
+ };
2418
+ }
2419
+
2420
+ function parseFrontmatterTools(frontmatter) {
2421
+ if (!frontmatter) return [];
2422
+ const lines = frontmatter.split(/\r?\n/);
2423
+ const tools = [];
2424
+ let collecting = false;
2425
+
2426
+ for (const line of lines) {
2427
+ const trimmed = line.trim();
2428
+ if (!trimmed) continue;
2429
+
2430
+ if (collecting) {
2431
+ if (trimmed.startsWith('- ')) {
2432
+ tools.push(trimmed.slice(2).trim());
2433
+ continue;
2434
+ }
2435
+ collecting = false;
2436
+ }
2437
+
2438
+ if (trimmed === 'tools:' || trimmed === 'allowed-tools:') {
2439
+ collecting = true;
2440
+ continue;
2441
+ }
2442
+
2443
+ if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) {
2444
+ const value = trimmed.slice(trimmed.indexOf(':') + 1).trim();
2445
+ if (value) {
2446
+ for (const tool of value.split(',')) {
2447
+ const name = tool.trim();
2448
+ if (name) tools.push(name);
2449
+ }
2450
+ } else {
2451
+ collecting = true;
2452
+ }
2453
+ }
2454
+ }
2455
+
2456
+ return tools;
2457
+ }
2458
+
2459
+ function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) {
2460
+ diagnostics.push({
2461
+ level: 'warning',
2462
+ code,
2463
+ message,
2464
+ value,
2465
+ source,
2466
+ });
2467
+ }
2468
+
2469
+ function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) {
2470
+ const result = mapClaudeToolsToKimiTools(toolNames, { source: sourceName });
2471
+ diagnostics.push(...result.diagnostics);
2472
+ return result.tools;
2473
+ }
2474
+
2475
+ function neutralizeKimiAgentPrompt(content) {
2476
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
2477
+ let prompt = frontmatter ? body : content;
2478
+ prompt = neutralizeAgentReferences(prompt, 'AGENTS.md');
2479
+ prompt = prompt.replace(/~\/\.claude\/gsd-core\b/g, 'GSD core');
2480
+ prompt = prompt.replace(/\$HOME\/\.claude\/gsd-core\b/g, 'GSD core');
2481
+ return prompt.replace(/^\s*\r?\n/, '');
2482
+ }
2483
+
2484
+ function pushKimiToolsYaml(lines, indent, tools) {
2485
+ const prefix = ' '.repeat(indent);
2486
+ if (!Array.isArray(tools) || tools.length === 0) {
2487
+ lines.push(`${prefix}tools: []`);
2488
+ return;
2489
+ }
2490
+ lines.push(`${prefix}tools:`);
2491
+ for (const tool of tools) {
2492
+ lines.push(`${prefix} - ${yamlQuote(tool)}`);
2493
+ }
2494
+ }
2495
+
2496
+ function buildKimiRootAgentYaml({ description, tools, subagents }) {
2497
+ const lines = [
2498
+ 'version: 1',
2499
+ 'agent:',
2500
+ ' name: gsd',
2501
+ ` description: ${yamlQuote(toSingleLine(description || 'Run GSD workflows in Kimi CLI.'))}`,
2502
+ ' extend: default',
2503
+ ' system_prompt_path: ./gsd.md',
2504
+ ];
2505
+ pushKimiToolsYaml(lines, 2, tools);
2506
+
2507
+ if (subagents.length > 0) {
2508
+ lines.push(' subagents:');
2509
+ for (const subagent of subagents) {
2510
+ lines.push(` ${subagent.name}:`);
2511
+ lines.push(` path: ./subagents/${subagent.name}.yaml`);
2512
+ lines.push(` description: ${yamlQuote(toSingleLine(subagent.description))}`);
2513
+ }
2514
+ }
2515
+
2516
+ return `${lines.join('\n')}\n`;
2517
+ }
2518
+
2519
+ function buildKimiSubagentYaml({ name, description, tools }) {
2520
+ const lines = [
2521
+ 'version: 1',
2522
+ 'agent:',
2523
+ ` name: ${name}`,
2524
+ ` description: ${yamlQuote(toSingleLine(description || `Run ${name}.`))}`,
2525
+ ` system_prompt_path: ./${name}.md`,
2526
+ ];
2527
+ pushKimiToolsYaml(lines, 2, tools);
2528
+ return `${lines.join('\n')}\n`;
2529
+ }
2530
+
2531
+ function buildKimiAgentArtifacts({
2532
+ rootAgent = '',
2533
+ subagents = [],
2534
+ requestedSubagents = null,
2535
+ } = {}) {
2536
+ const diagnostics = [];
2537
+ const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' };
2538
+ const { frontmatter: rootFrontmatter } = extractFrontmatterAndBody(rootSource.content);
2539
+ const rootDescription = rootFrontmatter
2540
+ ? extractFrontmatterField(rootFrontmatter, 'description') || 'Run GSD workflows in Kimi CLI.'
2541
+ : 'Run GSD workflows in Kimi CLI.';
2542
+
2543
+ const subagentSources = Array.isArray(subagents) ? subagents : [];
2544
+ if (!Array.isArray(subagents)) {
2545
+ addKimiAgentDiagnostic(
2546
+ diagnostics,
2547
+ 'kimi_unsupported_subagents_input',
2548
+ 'Subagents input must be an array of Markdown strings or source objects.',
2549
+ typeof subagents,
2550
+ null
2551
+ );
2552
+ }
2553
+
2554
+ const subagentMap = new Map();
2555
+ for (const source of subagentSources) {
2556
+ const parsed = parseKimiAgentSource(source);
2557
+ if (!parsed) {
2558
+ addKimiAgentDiagnostic(
2559
+ diagnostics,
2560
+ 'kimi_unsupported_subagent_input',
2561
+ 'Subagent source must be a Markdown string or an object with content.',
2562
+ typeof source,
2563
+ null
2564
+ );
2565
+ continue;
2566
+ }
2567
+
2568
+ const { frontmatter } = extractFrontmatterAndBody(parsed.content);
2569
+ const fallbackName = parsed.path ? path.basename(parsed.path, path.extname(parsed.path)) : null;
2570
+ const name = frontmatter
2571
+ ? extractFrontmatterField(frontmatter, 'name') || fallbackName
2572
+ : fallbackName;
2573
+ if (!name || !KIMI_CANONICAL_GSD_AGENT_RE.test(name)) {
2574
+ addKimiAgentDiagnostic(
2575
+ diagnostics,
2576
+ 'kimi_invalid_subagent_name',
2577
+ 'Subagent source does not use a canonical gsd-* Kimi agent name.',
2578
+ name || '(missing)',
2579
+ parsed.path
2580
+ );
2581
+ continue;
2582
+ }
2583
+
2584
+ const description = frontmatter
2585
+ ? extractFrontmatterField(frontmatter, 'description') || `Run ${name}.`
2586
+ : `Run ${name}.`;
2587
+ const tools = mapKimiAgentContractTools(parseFrontmatterTools(frontmatter), diagnostics, name);
2588
+ subagentMap.set(name, {
2589
+ name,
2590
+ description,
2591
+ tools,
2592
+ prompt: neutralizeKimiAgentPrompt(parsed.content),
2593
+ });
2594
+ }
2595
+
2596
+ const requested = Array.isArray(requestedSubagents) && requestedSubagents.length > 0
2597
+ ? requestedSubagents
2598
+ : [...subagentMap.keys()];
2599
+ const selectedSubagents = [];
2600
+ for (const requestedName of requested) {
2601
+ if (subagentMap.has(requestedName)) {
2602
+ selectedSubagents.push(subagentMap.get(requestedName));
2603
+ continue;
2604
+ }
2605
+ addKimiAgentDiagnostic(
2606
+ diagnostics,
2607
+ 'kimi_unknown_subagent',
2608
+ 'Requested subagent was not generated and will not be emitted in Kimi YAML.',
2609
+ requestedName,
2610
+ null
2611
+ );
2612
+ }
2613
+
2614
+ const rootTools = mapKimiAgentContractTools(parseFrontmatterTools(rootFrontmatter), diagnostics, 'gsd');
2615
+ if (selectedSubagents.length > 0 && !rootTools.includes('kimi_cli.tools.agent:Agent')) {
2616
+ rootTools.push('kimi_cli.tools.agent:Agent');
2617
+ }
2618
+
2619
+ return {
2620
+ root: {
2621
+ name: 'gsd',
2622
+ yamlPath: 'agents/gsd.yaml',
2623
+ promptPath: 'agents/gsd.md',
2624
+ yaml: buildKimiRootAgentYaml({
2625
+ description: rootDescription,
2626
+ tools: rootTools,
2627
+ subagents: selectedSubagents,
2628
+ }),
2629
+ prompt: neutralizeKimiAgentPrompt(rootSource.content),
2630
+ },
2631
+ subagents: selectedSubagents.map((subagent) => ({
2632
+ name: subagent.name,
2633
+ yamlPath: `agents/subagents/${subagent.name}.yaml`,
2634
+ promptPath: `agents/subagents/${subagent.name}.md`,
2635
+ yaml: buildKimiSubagentYaml(subagent),
2636
+ prompt: subagent.prompt,
2637
+ })),
2638
+ diagnostics,
2639
+ };
2640
+ }
2641
+
2228
2642
  /**
2229
2643
  * Convert a Claude agent (.md) to a Copilot agent (.agent.md).
2230
2644
  * Applies tool mapping + deduplication, formats tools as JSON array.
@@ -2561,6 +2975,14 @@ function convertClaudeToWindsurfMarkdown(content) {
2561
2975
  converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
2562
2976
  converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
2563
2977
  converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
2978
+ converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
2979
+ converted = converted.replace(/\.claude\//g, '.windsurf/');
2980
+ // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
2981
+ // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
2982
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
2983
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
2984
+ // Environment variable name rewrite
2985
+ converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
2564
2986
  // Remove Claude Code-specific bug workarounds before brand replacement
2565
2987
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
2566
2988
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
@@ -2782,6 +3204,12 @@ function convertClaudeToTraeMarkdown(content) {
2782
3204
  converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
2783
3205
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
2784
3206
  converted = converted.replace(/\.claude\//g, '.trae/');
3207
+ // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
3208
+ // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
3209
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae');
3210
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae');
3211
+ // Environment variable name rewrite
3212
+ converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
2785
3213
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
2786
3214
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
2787
3215
  converted = converted.replace(/\bClaude Code\b/g, 'Trae');
@@ -3009,6 +3437,16 @@ function convertSlashCommandsToCodexSkillMentions(content) {
3009
3437
  return converted;
3010
3438
  }
3011
3439
 
3440
+ const CODEX_GSD_TOOLS_INVOCATION = 'node "$HOME/.codex/gsd-core/bin/gsd-tools.cjs"';
3441
+
3442
+ function rewriteBareGsdToolsCommandsForCodex(content) {
3443
+ return content
3444
+ .replace(/(^[ \t]*)gsd-tools(?=\s)/gm, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
3445
+ .replace(/(\$\(\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
3446
+ .replace(/(`\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`)
3447
+ .replace(/((?:&&|\|\||[;|])\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`);
3448
+ }
3449
+
3012
3450
  function convertClaudeToCodexMarkdown(content) {
3013
3451
  let converted = convertSlashCommandsToCodexSkillMentions(content);
3014
3452
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
@@ -3034,6 +3472,10 @@ function convertClaudeToCodexMarkdown(content) {
3034
3472
  // `.claudeignore` → `.codexignore` (#2639). Codex honors its own ignore
3035
3473
  // file; leaving the Claude-specific name is misleading in agent prompts.
3036
3474
  converted = converted.replace(/\.claudeignore\b/g, '.codexignore');
3475
+ // Codex installs the tools shim under ~/.codex but does not guarantee a
3476
+ // bare `gsd-tools` binary on PATH. Keep resolver probes such as
3477
+ // `command -v gsd-tools` intact; rewrite only command invocations.
3478
+ converted = rewriteBareGsdToolsCommandsForCodex(converted);
3037
3479
  // Runtime-neutral agent name replacement (#766)
3038
3480
  converted = neutralizeAgentReferences(converted, 'AGENTS.md');
3039
3481
  return converted;
@@ -3073,7 +3515,13 @@ Execute mode fallback:
3073
3515
  ## C. Task() → spawn_agent Mapping
3074
3516
  GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
3075
3517
 
3076
- Direct mapping:
3518
+ **Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas:
3519
+ - **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available.
3520
+ - **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session.
3521
+
3522
+ Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active.
3523
+
3524
+ Typed mapping (agent_type-capable schema only):
3077
3525
  - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
3078
3526
  - \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
3079
3527
  - \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
@@ -3090,6 +3538,24 @@ Direct mapping:
3090
3538
  Workflows that require this isolation must fail closed or use an explicit
3091
3539
  manual worktree protocol before spawning (#3360).
3092
3540
 
3541
+ Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
3542
+ When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
3543
+ (\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
3544
+ (openai/codex#15250). **This workaround is NOT equivalent to typed gsd-planner/gsd-executor
3545
+ execution** — GSD agents carry project-aware prompts, audit logging, and workflow context
3546
+ that a generic subagent lacks. Use the following fallback:
3547
+ 1. Resolve your active Codex config root — the directory that contains your \`config.toml\`.
3548
+ This directory is determined in priority order: \`$CODEX_HOME\` (if set), the path given
3549
+ by \`--config-dir\` (if passed on invocation), a local \`.codex\` directory in the current
3550
+ project (if \`--local\` was used), or the default global config directory. Read
3551
+ \`agents/<agent-name>.toml\` relative to that config root to extract the agent's system
3552
+ instructions.
3553
+ 2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call.
3554
+ 3. Label results and logs clearly as "generic-agent workaround" so the orchestrator and user
3555
+ know full typed-agent guarantees are not in effect.
3556
+ 4. Where typed dispatch is mandatory for correctness (e.g. worktree isolation), fail closed
3557
+ and report the schema limitation rather than silently degrading.
3558
+
3093
3559
  Spawn restriction:
3094
3560
  - Codex restricts \`spawn_agent\` to cases where the user has explicitly
3095
3561
  requested sub-agents. When automatic spawning is not permitted, do the
@@ -3182,14 +3648,17 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3182
3648
  // Task() model parameters). See #2256.
3183
3649
  // Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
3184
3650
  const modelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
3651
+ let hasPinnedModel = false;
3185
3652
  if (modelOverride) {
3186
3653
  lines.push(`model = ${JSON.stringify(modelOverride)}`);
3654
+ hasPinnedModel = true;
3187
3655
  } else if (runtimeResolver) {
3188
3656
  // #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
3189
3657
  // from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
3190
3658
  const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
3191
3659
  if (entry?.model) {
3192
3660
  lines.push(`model = ${JSON.stringify(entry.model)}`);
3661
+ hasPinnedModel = true;
3193
3662
  // model is resolved here; reasoning_effort from catalog tier is REPLACED by the
3194
3663
  // unified effort resolver below (#443). Do NOT emit entry.reasoning_effort here.
3195
3664
  }
@@ -3200,9 +3669,15 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3200
3669
  // from the same effort.agent_overrides / effort.routing_tier_defaults / effort.default
3201
3670
  // config source. Codex does not support 'max' → clamped to 'xhigh' by
3202
3671
  // gsdRenderEffortForRuntime('codex', ...).
3203
- const _universalEffortCodex = resolveInstallTimeEffort(effortCfg, resolvedName !== agentName ? resolvedName : agentName);
3204
- const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
3205
- lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
3672
+ // #838 — Do not pin effort when Codex is intentionally inheriting the parent
3673
+ // chat model. A TOML with no `model` but a static `model_reasoning_effort`
3674
+ // creates confusing partial routing: model follows the Codex UI while effort
3675
+ // follows GSD. Keep those knobs coupled unless GSD also pins the model.
3676
+ if (hasPinnedModel) {
3677
+ const _universalEffortCodex = resolveInstallTimeEffort(effortCfg, resolvedName !== agentName ? resolvedName : agentName);
3678
+ const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
3679
+ lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
3680
+ }
3206
3681
 
3207
3682
  // #774 — Emit service_tier and model_verbosity for light-tier agents.
3208
3683
  // Light-tier agents (routingTier: "light" in model-catalog.json) are haiku-equivalent
@@ -7100,7 +7575,7 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) {
7100
7575
  const fullPath = path.join(dir, entry.name);
7101
7576
  if (entry.isDirectory()) {
7102
7577
  walkAndRewrite(fullPath);
7103
- } else if (entry.name === 'SKILL.md') {
7578
+ } else if (entry.name.endsWith('.md')) {
7104
7579
  let content = fs.readFileSync(fullPath, 'utf8');
7105
7580
  content = _applyRuntimeRewrites(content, runtime, pathPrefix);
7106
7581
  fs.writeFileSync(fullPath, content);
@@ -7200,6 +7675,11 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) {
7200
7675
  content = content.replace(/~\/\.claude\//g, pathPrefix);
7201
7676
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
7202
7677
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
7678
+ // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
7679
+ // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
7680
+ // fires between 'e' and '-', which rewrites .claude-plugin → .windsurf-plugin).
7681
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
7682
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
7203
7683
  content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
7204
7684
  content = processAttribution(content, getCommitAttribution(runtime));
7205
7685
  break;
@@ -7291,6 +7771,16 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) {
7291
7771
  content = processAttribution(content, getCommitAttribution(runtime));
7292
7772
  break;
7293
7773
 
7774
+ case 'kimi':
7775
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
7776
+ content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
7777
+ content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
7778
+ content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
7779
+ content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
7780
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7781
+ content = processAttribution(content, getCommitAttribution(runtime));
7782
+ break;
7783
+
7294
7784
  default:
7295
7785
  // Unknown runtime — no rewrites.
7296
7786
  // OpenCode/Kilo are intentionally absent: their skills are written by
@@ -7314,6 +7804,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) {
7314
7804
  * encodes the GSD namespace as its last segment (e.g. `commands/gsd`), in
7315
7805
  * which case write as `${stem}.md` (directory IS the namespace).
7316
7806
  * - agents: write as-is (files already carry their own `gsd-` prefix).
7807
+ * For kimi-agents kind: recursively copy generated YAML/prompt files.
7317
7808
  */
7318
7809
  function _copyStaged(stagedDir, destDir, kind) {
7319
7810
  if (!fs.existsSync(stagedDir)) return;
@@ -7330,6 +7821,11 @@ function _copyStaged(stagedDir, destDir, kind) {
7330
7821
  return;
7331
7822
  }
7332
7823
 
7824
+ if (kind.kind === 'kimi-agents') {
7825
+ fs.cpSync(stagedDir, destDir, { recursive: true });
7826
+ return;
7827
+ }
7828
+
7333
7829
  // commands or agents
7334
7830
  const entries = fs.readdirSync(stagedDir, { withFileTypes: true });
7335
7831
  // For commands: apply prefix unless the destSubpath's last segment already
@@ -7361,11 +7857,27 @@ function _copyStaged(stagedDir, destDir, kind) {
7361
7857
 
7362
7858
  /**
7363
7859
  * Remove GSD-prefixed entries from destDir matching kind.prefix.
7364
- * For Hermes nested case (prefix === ''): the destSubpath IS the namespace
7365
- * (skills/gsd) — remove the entire destDir.
7860
+ * For the prefix='' case: the destSubpath IS the namespace — remove the entire
7861
+ * destDir. (No current runtime uses prefix='' after #947 reversed Hermes; kept
7862
+ * as a defensive guard for future runtimes.)
7366
7863
  */
7367
7864
  function _removeGsdEntries(destDir, kind) {
7368
7865
  if (!fs.existsSync(destDir)) return;
7866
+ if (kind.kind === 'kimi-agents') {
7867
+ for (const fileName of ['gsd.yaml', 'gsd.md']) {
7868
+ fs.rmSync(path.join(destDir, fileName), { force: true });
7869
+ }
7870
+ const subagentsDir = path.join(destDir, 'subagents');
7871
+ if (fs.existsSync(subagentsDir)) {
7872
+ for (const entry of fs.readdirSync(subagentsDir, { withFileTypes: true })) {
7873
+ if (!entry.isFile()) continue;
7874
+ if (!entry.name.startsWith('gsd-')) continue;
7875
+ if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue;
7876
+ fs.rmSync(path.join(subagentsDir, entry.name), { force: true });
7877
+ }
7878
+ }
7879
+ return;
7880
+ }
7369
7881
  if (kind.prefix === '') {
7370
7882
  // Whole-namespace removal (Hermes nested case — destSubpath is skills/gsd)
7371
7883
  // The directory itself is the GSD namespace, so remove it entirely.
@@ -7419,20 +7931,11 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
7419
7931
  }
7420
7932
  }
7421
7933
 
7422
- // Hermes: remove intermediate-layout skills/gsd/gsd-*/ entries that existed
7423
- // between #2841 and #3664. Phase 2 (#3664) uses prefix='' producing bare-stem
7424
- // names (skills/gsd/<stem>/SKILL.md); the intermediate layout had the gsd-
7425
- // prefix inside the nested dir (skills/gsd/gsd-<stem>/SKILL.md). Only
7426
- // children whose name starts with gsd- are removed — the parent skills/gsd/
7427
- // directory and any non-gsd- siblings (user content) are preserved.
7428
- const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
7429
- if (fs.existsSync(nestedGsdDir)) {
7430
- for (const entry of fs.readdirSync(nestedGsdDir, { withFileTypes: true })) {
7431
- if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
7432
- fs.rmSync(path.join(nestedGsdDir, entry.name), { recursive: true });
7433
- }
7434
- }
7435
- }
7934
+ // Hermes: bare-stem skills/gsd/<stem>/ cleanup is deferred to AFTER the
7935
+ // layout-driven install loop in installRuntimeArtifacts, where the exact set
7936
+ // of staged gsd-<stem>/ dirs is known. Removing here (before staging) would
7937
+ // require readGsdCommandNames() which misses skills like 'dev-preferences'
7938
+ // that are not in the commands directory. See _removeHermesBareStemDirs().
7436
7939
  }
7437
7940
 
7438
7941
  // Migrate dev-preferences.md content → runtime-aware SKILL.md location (#2973).
@@ -7489,6 +7992,18 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
7489
7992
  }
7490
7993
  }
7491
7994
  }
7995
+
7996
+ // Hermes: pre-#947 bare-stem skills/gsd/<stem>/ entries (dirs that do NOT
7997
+ // start with 'gsd-') — the #3664 layout used prefix='' so GSD-owned skills
7998
+ // had bare names (e.g. skills/gsd/help/). These are stale on uninstall.
7999
+ const nestedGsdDirForUninstall = path.join(configDir, 'skills', 'gsd');
8000
+ if (fs.existsSync(nestedGsdDirForUninstall)) {
8001
+ for (const entry of fs.readdirSync(nestedGsdDirForUninstall, { withFileTypes: true })) {
8002
+ if (entry.isDirectory() && !entry.name.startsWith('gsd-')) {
8003
+ fs.rmSync(path.join(nestedGsdDirForUninstall, entry.name), { recursive: true });
8004
+ }
8005
+ }
8006
+ }
7492
8007
  }
7493
8008
 
7494
8009
  // Return saved artifacts so the caller can migrate after layout-driven removal.
@@ -7539,6 +8054,43 @@ function _restoreDir(dir, snapshot) {
7539
8054
  }
7540
8055
  }
7541
8056
 
8057
+ /**
8058
+ * After the layout-driven install loop writes new gsd-<stem>/ dirs to
8059
+ * skills/gsd/, remove any pre-existing bare-stem dirs (skills/gsd/<stem>/)
8060
+ * that correspond to the newly installed gsd-<stem> entries.
8061
+ *
8062
+ * The removal set is derived from the ACTUAL installed skill dirs (every
8063
+ * entry starting with 'gsd-' that is a directory), so it covers ALL shipped
8064
+ * GSD skills — including 'dev-preferences' and future additions — without
8065
+ * relying on readGsdCommandNames() which only enumerates the commands source
8066
+ * tree and can miss skills that ship outside that directory.
8067
+ *
8068
+ * Safety: a bare dir is ONLY removed when a corresponding gsd-<stem>/ dir was
8069
+ * installed this run. A user-owned dir 'skills/gsd/my-workflow/' that has no
8070
+ * matching 'skills/gsd/gsd-my-workflow/' is never touched.
8071
+ *
8072
+ * @param {string} nestedGsdDir absolute path to skills/gsd/ category dir
8073
+ */
8074
+ function _removeHermesBareStemDirs(nestedGsdDir) {
8075
+ if (!fs.existsSync(nestedGsdDir)) return;
8076
+ const entries = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
8077
+
8078
+ // Collect the set of stems that were installed as gsd-<stem>/ this run.
8079
+ const installedStems = new Set();
8080
+ for (const entry of entries) {
8081
+ if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
8082
+ installedStems.add(entry.name.slice('gsd-'.length)); // e.g. 'quick', 'dev-preferences'
8083
+ }
8084
+ }
8085
+
8086
+ // Remove any bare <stem>/ dir for which gsd-<stem>/ was just installed.
8087
+ for (const entry of entries) {
8088
+ if (entry.isDirectory() && !entry.name.startsWith('gsd-') && installedStems.has(entry.name)) {
8089
+ fs.rmSync(path.join(nestedGsdDir, entry.name), { recursive: true });
8090
+ }
8091
+ }
8092
+ }
8093
+
7542
8094
  function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7543
8095
  // Legacy cleanup before layout-driven writes
7544
8096
  _runLegacyInstallMigrations(runtime, configDir, scope);
@@ -7562,7 +8114,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7562
8114
  // stagedForCopy: the directory to copy from (may differ from staged if rewrites
7563
8115
  // produce a temp copy — see applyRuntimeContentRewritesForCommandsInPlace).
7564
8116
  let stagedForCopy = staged;
7565
- if (kind.kind === 'skills') {
8117
+ if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
7566
8118
  applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix);
7567
8119
  } else if (kind.kind === 'commands') {
7568
8120
  // Returns a temp dir with rewritten content so source files are never mutated.
@@ -7579,29 +8131,15 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7579
8131
  // then restore after. This preserves user dirs across a wipe-and-replace
7580
8132
  // install (#2973 / #3664).
7581
8133
  //
7582
- // For prefix='' (Hermes): _removeGsdEntries wipes the entire dest dir (skills/gsd/).
7583
- // Preserve every subdir that is NOT in the staged set — those are user-added dirs
7584
- // (e.g. user-content/) that GSD does not manage.
7585
- //
7586
- // For prefix='gsd-' (others): _removeGsdEntries removes only gsd-* entries.
7587
- // Non-gsd-* user dirs (e.g. my-custom-skill/) are untouched. Only preserve the
7588
- // explicit user-owned GSD-prefixed skill gsd-dev-preferences, which GSD does not
7589
- // reinstall from source but must survive the prune (#2973).
8134
+ // All runtimes (incl. Hermes after #947) use prefix='gsd-'.
8135
+ // _removeGsdEntries removes only gsd-* entries; non-gsd-* user dirs are
8136
+ // untouched. Preserve the explicit user-owned GSD-prefixed skill
8137
+ // gsd-dev-preferences, which GSD does not reinstall from source but must
8138
+ // survive the prune (#2973).
7590
8139
  const toPreserve = new Map(); // dirName -> Map<relPath, Buffer>
7591
8140
 
7592
- if (kind.prefix === '') {
7593
- // Hermes: wipes entire dest dir — preserve anything not in staged.
7594
- const stagedNames = fs.existsSync(stagedForCopy)
7595
- ? new Set(fs.readdirSync(stagedForCopy, { withFileTypes: true })
7596
- .filter(e => e.isDirectory()).map(e => e.name))
7597
- : new Set();
7598
- for (const entry of fs.readdirSync(dest, { withFileTypes: true })) {
7599
- if (!entry.isDirectory() || stagedNames.has(entry.name)) continue;
7600
- const snap = _snapshotDir(path.join(dest, entry.name));
7601
- if (snap.size > 0) toPreserve.set(entry.name, snap);
7602
- }
7603
- } else {
7604
- // Non-Hermes: only preserve explicitly user-owned GSD-prefixed skill dirs.
8141
+ {
8142
+ // Preserve explicitly user-owned GSD-prefixed skill dirs.
7605
8143
  // gsd-dev-preferences is the sole user-customisable skill in this category.
7606
8144
  const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
7607
8145
  for (const dirName of USER_OWNED_SKILL_DIRS) {
@@ -7631,6 +8169,21 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7631
8169
  }
7632
8170
  }
7633
8171
  }
8172
+
8173
+ // Hermes: after the install loop has written all gsd-<stem>/ dirs to
8174
+ // skills/gsd/, remove any stale bare-stem dirs (skills/gsd/<stem>/) that
8175
+ // correspond to the newly installed gsd-<stem> entries. This is the robust
8176
+ // replacement for the readGsdCommandNames()-based pre-install cleanup that
8177
+ // missed skills like 'dev-preferences' (#947 adversarial review).
8178
+ //
8179
+ // We run this AFTER the install loop so the installed set is authoritative:
8180
+ // every gsd-<stem>/ present now was written this run (or was there before
8181
+ // with the same prefix). User-owned bare dirs with no gsd-<stem> counterpart
8182
+ // are untouched.
8183
+ if (runtime === 'hermes') {
8184
+ const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd');
8185
+ _removeHermesBareStemDirs(nestedGsdDirForCleanup);
8186
+ }
7634
8187
  }
7635
8188
 
7636
8189
  /**
@@ -7736,6 +8289,23 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
7736
8289
  _removeGsdEntries(dest, kind);
7737
8290
  }
7738
8291
 
8292
+ // Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
8293
+ // the GSD-managed DESCRIPTION.md and then the category dir itself if it
8294
+ // contains no user content (#947). _removeGsdEntries removed gsd-* dirs
8295
+ // but left the category container and DESCRIPTION.md intact.
8296
+ if (runtime === 'hermes') {
8297
+ const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
8298
+ if (fs.existsSync(nestedGsdDir)) {
8299
+ // Remove GSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
8300
+ fs.rmSync(path.join(nestedGsdDir, 'DESCRIPTION.md'), { force: true });
8301
+ // Remove the category dir if empty (no user content remaining)
8302
+ const remaining = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
8303
+ if (remaining.length === 0) {
8304
+ fs.rmSync(nestedGsdDir, { recursive: true, force: true });
8305
+ }
8306
+ }
8307
+ }
8308
+
7739
8309
  // #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the
7740
8310
  // runtime-aware SKILL.md location after all layout-driven removal is
7741
8311
  // complete. Do NOT restore to commands/gsd/ — the user is uninstalling.
@@ -7795,6 +8365,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7795
8365
  content = content.replace(globalClaudeRegex, pathPrefix);
7796
8366
  content = content.replace(globalClaudeHomeRegex, pathPrefix);
7797
8367
  content = content.replace(localClaudeRegex, `./${dirName}/`);
8368
+ content = content.replace(/~\/\.claude\b/g, pathPrefix.replace(/\/$/, ''));
8369
+ content = content.replace(/\$HOME\/\.claude\b/g, pathPrefix.replace(/\/$/, ''));
8370
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7798
8371
  content = content.replace(/~\/\.qwen\//g, pathPrefix);
7799
8372
  content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
7800
8373
  content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
@@ -8130,6 +8703,7 @@ function uninstall(isGlobal, runtime = 'claude') {
8130
8703
  if (runtime === 'trae') runtimeLabel = 'Trae';
8131
8704
  if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
8132
8705
  if (runtime === 'hermes') runtimeLabel = 'Hermes Agent';
8706
+ if (runtime === 'kimi') runtimeLabel = 'Kimi CLI';
8133
8707
  if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy';
8134
8708
 
8135
8709
  console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
@@ -9109,6 +9683,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9109
9683
  const isWindsurf = runtime === 'windsurf';
9110
9684
  const isTrae = runtime === 'trae';
9111
9685
  const isCline = runtime === 'cline';
9686
+ const isKimi = runtime === 'kimi';
9112
9687
  const isHermes = runtime === 'hermes';
9113
9688
  const gsdDir = path.join(configDir, 'gsd-core');
9114
9689
  const commandsDir = path.join(configDir, 'commands', 'gsd');
@@ -9156,8 +9731,8 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9156
9731
  }
9157
9732
  }
9158
9733
  if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
9159
- // Hermes uses prefix '' (bare stem names); all others use 'gsd-'
9160
- const skillListPrefix = isHermes ? '' : 'gsd-';
9734
+ // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
9735
+ const skillListPrefix = 'gsd-';
9161
9736
  for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
9162
9737
  const skillRoot = path.join(codexSkillsDir, skillName);
9163
9738
  const skillHashes = generateManifest(skillRoot);
@@ -9173,7 +9748,16 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9173
9748
  }
9174
9749
  }
9175
9750
  }
9176
- if (fs.existsSync(agentsDir)) {
9751
+ if (isKimi && fs.existsSync(agentsDir)) {
9752
+ const agentHashes = generateManifest(agentsDir);
9753
+ for (const [rel, hash] of Object.entries(agentHashes)) {
9754
+ const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md';
9755
+ const isSubagent = /^subagents\/gsd-[^/]+\.(yaml|md)$/.test(rel);
9756
+ if (isRootAgent || isSubagent) {
9757
+ manifest.files['agents/' + rel] = hash;
9758
+ }
9759
+ }
9760
+ } else if (fs.existsSync(agentsDir)) {
9177
9761
  for (const file of fs.readdirSync(agentsDir)) {
9178
9762
  if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
9179
9763
  manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
@@ -9194,12 +9778,20 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9194
9778
 
9195
9779
  // Track hook files so saveLocalPatches() can detect user modifications
9196
9780
  // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
9197
- if (!isCodex && !isCopilot && !isCline) {
9781
+ if (!isCodex && !isCopilot && !isCline && !isKimi) {
9198
9782
  const hooksDir = path.join(configDir, 'hooks');
9199
9783
  if (fs.existsSync(hooksDir)) {
9200
- for (const file of fs.readdirSync(hooksDir)) {
9201
- if (file.startsWith('gsd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
9202
- manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
9784
+ // Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
9785
+ // scripts/build-hooks.js) rather than a prefix/extension regex, so the
9786
+ // manifest set is structurally identical to the build set. The old regex
9787
+ // `file.startsWith('gsd-') && (file.endsWith('.js') || file.endsWith('.sh'))`
9788
+ // missed managed-hooks-registry.cjs (wrong prefix, .cjs extension), causing
9789
+ // detect-custom-files to flag it as a perpetual false-positive custom file
9790
+ // on every /gsd-update. See #941.
9791
+ for (const hook of INSTALLED_HOOK_FILES) {
9792
+ const hookPath = path.join(hooksDir, hook);
9793
+ if (fs.existsSync(hookPath)) {
9794
+ manifest.files['hooks/' + hook] = fileHash(hookPath);
9203
9795
  }
9204
9796
  }
9205
9797
  // Track hooks/lib/ helpers so saveLocalPatches() can back up user edits
@@ -9530,6 +10122,8 @@ function reportLocalPatches(configDir, runtime = 'claude') {
9530
10122
  ? '$gsd-update --reapply'
9531
10123
  : runtime === 'cursor'
9532
10124
  ? 'gsd-update --reapply (mention the skill name)'
10125
+ : runtime === 'kimi'
10126
+ ? '/skill:gsd-update --reapply'
9533
10127
  : '/gsd-update --reapply';
9534
10128
  console.log('');
9535
10129
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
@@ -9560,6 +10154,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9560
10154
  const isOpencode = runtime === 'opencode';
9561
10155
  const isGemini = runtime === 'gemini';
9562
10156
  const isKilo = runtime === 'kilo';
10157
+ const isKimi = runtime === 'kimi';
9563
10158
  const isCodex = runtime === 'codex';
9564
10159
  const isCopilot = runtime === 'copilot';
9565
10160
  const isAntigravity = runtime === 'antigravity';
@@ -9575,6 +10170,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9575
10170
  const dirName = getDirName(runtime);
9576
10171
  const src = path.join(__dirname, '..');
9577
10172
 
10173
+ if (isKimi && !isGlobal) {
10174
+ console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
10175
+ console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
10176
+ console.log(` Project-level Kimi install semantics remain deferred.`);
10177
+ return {
10178
+ runtime,
10179
+ skipped: true,
10180
+ reason: 'kimi_local_deferred',
10181
+ configDir: null,
10182
+ settingsPath: null,
10183
+ settings: null,
10184
+ statuslineCommand: null,
10185
+ updateBannerCommand: null,
10186
+ rollbackInstallerMigrations: () => {},
10187
+ };
10188
+ }
10189
+
9578
10190
  // Reusable helper to copy hooks/lib/ (git-cmd.js + gsd-graphify-rebuild.sh).
9579
10191
  // Defined early so it is visible to both the main and Codex code paths.
9580
10192
  // `allowlist` (when non-empty) restricts copying to the named top-level entries,
@@ -9629,7 +10241,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9629
10241
  // @-references resolve correctly (#2376 Windows, #2831 macOS/Linux).
9630
10242
  // gsd update marker re-application (ADR-0010 Deviation 2):
9631
10243
  // Resolve which profile to use for this runtime's install:
9632
- // 1. --minimal / --core-only → back-compat path (stageSkillsForMode keeps strict core allowlist)
10244
+ // 1. --minimal / --core-only → back-compat alias for the core profile
9633
10245
  // 2. Explicit --profile=<name> → use it (overrides any marker)
9634
10246
  // 3. Marker exists in targetDir → honor it (prevents silent expansion on update)
9635
10247
  // 4. Else → 'full' (back-compat for fresh non-interactive installs)
@@ -9638,8 +10250,16 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9638
10250
  // differ, the caller may use mostRestrictiveProfile() across the per-runtime
9639
10251
  // results — here we resolve each runtime independently.
9640
10252
  //
9641
- // Note: --minimal uses stageSkillsForMode (back-compat: strict allowlist, no closure).
9642
- // Named profiles (--profile=X or marker-driven) use resolveProfile() for transitive closure.
10253
+ // ADR-857 phase 4c: ALL profiles (including core/minimal) use stageSkillsForProfile
10254
+ // with the registry-aware _resolvedProfile so future tier:core capabilities are
10255
+ // staged on core installs. The 'minimal' back-compat distinction is now ONLY the
10256
+ // empty manifest (core profile has no transitive deps); the registry IS consulted.
10257
+ // MINIMAL is intentionally the same skill set as the 'core' profile
10258
+ // (MINIMAL_ALLOWLIST_SET === Set(PROFILES.core)) — it is NOT a separately curated
10259
+ // subset. Any future tier:core capability therefore DOES belong in a minimal/core
10260
+ // install. Using stageSkillsForProfile(_resolvedProfile) honors the registry while
10261
+ // keeping the effective skill set identical to the prior stageSkillsForMode path
10262
+ // until a tier:core capability is registered.
9643
10263
  const _activeProfileName = hasMinimal
9644
10264
  ? 'core' // --minimal is a back-compat alias for the core profile; marker records 'core'
9645
10265
  : resolveEffectiveProfile({
@@ -9650,19 +10270,18 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9650
10270
  const _effectiveInstallMode = _isCoreProfileAlias ? 'minimal' : 'full';
9651
10271
  // Load the manifest and compute resolved profile for named profiles.
9652
10272
  // For --minimal/core: use an empty manifest (core profile has no transitive
9653
- // deps) to produce a resolvedProfile with the core skill set. This allows
9654
- // installRuntimeArtifacts to use stageSkillsForProfile uniformly across all
9655
- // profile modes without a null sentinel.
10273
+ // deps) to produce a resolvedProfile with the core skill set. Registry IS
10274
+ // consulted so tier:core capability skills are included when registered.
9656
10275
  const _commandsDir = path.join(src, 'commands', 'gsd');
9657
10276
  const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
9658
10277
  const _resolvedProfile = resolveProfile({
9659
10278
  modes: [_activeProfileName],
9660
10279
  manifest: _skillsManifest,
10280
+ registry: _capabilityRegistry,
9661
10281
  });
9662
- // Unified staging function: for --minimal uses stageSkillsForMode (back-compat);
9663
- // for named profiles uses stageSkillsForProfile (new API with transitive closure).
10282
+ // Unified staging function: all profiles use stageSkillsForProfile with the
10283
+ // registry-aware _resolvedProfile (ADR-857 phase 4c cutover).
9664
10284
  function _stageSkills(commandsGsdDir) {
9665
- if (_isCoreProfileAlias) return stageSkillsForMode(commandsGsdDir, _effectiveInstallMode);
9666
10285
  return stageSkillsForProfile(commandsGsdDir, _resolvedProfile);
9667
10286
  }
9668
10287
  function _stageAgents(agentsDir) {
@@ -9701,6 +10320,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9701
10320
  if (isTrae) runtimeLabel = 'Trae';
9702
10321
  if (isQwen) runtimeLabel = 'Qwen Code';
9703
10322
  if (isHermes) runtimeLabel = 'Hermes Agent';
10323
+ if (isKimi) runtimeLabel = 'Kimi';
9704
10324
  if (isCodebuddy) runtimeLabel = 'CodeBuddy';
9705
10325
  if (isCline) runtimeLabel = 'Cline';
9706
10326
 
@@ -9990,6 +10610,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9990
10610
  // Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782).
9991
10611
  const _isSkillsRuntime = isCodex || isCopilot || isAntigravity || isCursor || isWindsurf ||
9992
10612
  isAugment || isTrae || isCodebuddy || isQwen || isHermes ||
10613
+ isKimi ||
9993
10614
  (runtime === 'claude' && isGlobal) ||
9994
10615
  (isCline && isGlobal);
9995
10616
 
@@ -10017,9 +10638,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10017
10638
  if (isHermes) {
10018
10639
  const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
10019
10640
  if (fs.existsSync(hermesSkillsDir)) {
10020
- // Hermes layout uses prefix: '' — skill dirs have bare stem names (no gsd- prefix)
10641
+ // Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
10021
10642
  const count = fs.readdirSync(hermesSkillsDir, { withFileTypes: true })
10022
- .filter(e => e.isDirectory()).length;
10643
+ .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
10023
10644
  if (count > 0) {
10024
10645
  console.log(` ${green}✓${reset} Installed ${count} skills to skills/gsd/`);
10025
10646
  } else {
@@ -10028,6 +10649,26 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10028
10649
  } else {
10029
10650
  failures.push('skills/gsd/*');
10030
10651
  }
10652
+ } else if (isKimi) {
10653
+ const skillsDir = path.join(targetDir, 'skills');
10654
+ const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
10655
+ if (fs.existsSync(skillsDir)) {
10656
+ const count = fs.readdirSync(skillsDir, { withFileTypes: true })
10657
+ .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
10658
+ if (count > 0) {
10659
+ console.log(` ${green}✓${reset} Installed ${count} Kimi skills to skills/`);
10660
+ } else {
10661
+ failures.push('skills/gsd-*');
10662
+ }
10663
+ } else {
10664
+ failures.push('skills/gsd-*');
10665
+ }
10666
+ if (fs.existsSync(rootAgentPath)) {
10667
+ console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`);
10668
+ console.log(` Launch with: kimi --agent-file ${rootAgentPath}`);
10669
+ } else {
10670
+ failures.push('agents/gsd.yaml');
10671
+ }
10031
10672
  } else {
10032
10673
  const skillsDir = path.join(targetDir, 'skills');
10033
10674
  if (fs.existsSync(skillsDir)) {
@@ -10264,7 +10905,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10264
10905
  }
10265
10906
  }
10266
10907
 
10267
- if (isMinimalMode(_effectiveInstallMode)) {
10908
+ if (isKimi) {
10909
+ console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`);
10910
+ } else if (isMinimalMode(_effectiveInstallMode)) {
10268
10911
  // Codex registers agents in `config.toml` via `[agents.gsd-*]` sections.
10269
10912
  // Without stripping them here, a full → minimal reinstall would leave the
10270
10913
  // runtime advertising the old full agent surface even though the agent
@@ -10407,7 +11050,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10407
11050
  failures.push('VERSION');
10408
11051
  }
10409
11052
 
10410
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
11053
+ if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) {
10411
11054
  // Write package.json to force CommonJS mode for GSD scripts
10412
11055
  // Prevents "require is not defined" errors when project has "type": "module"
10413
11056
  // Node.js walks up looking for package.json - this stops inheritance from project
@@ -10503,7 +11146,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10503
11146
  // the hooks/lib/ helpers — otherwise the Codex comment downstream
10504
11147
  // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice.
10505
11148
  const hooksLibSrc = path.join(src, 'hooks', 'lib');
10506
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && fs.existsSync(hooksLibSrc)) {
11149
+ if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) {
10507
11150
  const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
10508
11151
  fs.mkdirSync(hooksLibDest, { recursive: true });
10509
11152
  copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
@@ -11089,7 +11732,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11089
11732
  }
11090
11733
 
11091
11734
  if (configIntent.installSurface === 'profile-marker-only') {
11092
- // Windsurf/Trae use skills — no config.toml, no settings.json hooks needed
11735
+ // Windsurf/Trae/Kimi use artifact-only surfaces — no config.toml or settings.json hooks needed.
11093
11736
  persistActiveProfileMarker();
11094
11737
  return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11095
11738
  }
@@ -11282,6 +11925,18 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11282
11925
  }
11283
11926
  }
11284
11927
 
11928
+ // Helper: detect whether a hook entry references a managed hook by name.
11929
+ // Checks both the plain command string (standard form) and the args array
11930
+ // (command+args / wrapped-launcher form used by windowless launchers on
11931
+ // Windows and some custom PATH-less environments). Without this check the
11932
+ // presence guards below only inspect h.command, so an args-form wrapper is
11933
+ // invisible and a stock string-command entry is appended on every
11934
+ // install/update, running the hook twice. (#976)
11935
+ function referencesHook(h, hookName) {
11936
+ return (typeof h.command === 'string' && h.command.includes(hookName)) ||
11937
+ (Array.isArray(h.args) && h.args.some(a => typeof a === 'string' && a.includes(hookName)));
11938
+ }
11939
+
11285
11940
  // Configure SessionStart hook for update checking (skip for opencode)
11286
11941
  if (!isOpencode && !isKilo) {
11287
11942
  if (!settings.hooks) {
@@ -11292,7 +11947,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11292
11947
  }
11293
11948
 
11294
11949
  const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
11295
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
11950
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-check-update'))
11296
11951
  );
11297
11952
 
11298
11953
  // Guard: only register if the hook file was actually installed (#1754).
@@ -11320,7 +11975,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11320
11975
  }
11321
11976
 
11322
11977
  const hasContextMonitorHook = settings.hooks[postToolEvent].some(entry =>
11323
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))
11978
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-context-monitor'))
11324
11979
  );
11325
11980
 
11326
11981
  const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
@@ -11341,14 +11996,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11341
11996
  } else {
11342
11997
  // Migrate existing context monitor hooks: add matcher and timeout if missing
11343
11998
  for (const entry of settings.hooks[postToolEvent]) {
11344
- if (entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))) {
11999
+ if (entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-context-monitor'))) {
11345
12000
  let migrated = false;
11346
12001
  if (!entry.matcher) {
11347
12002
  entry.matcher = 'Bash|Edit|Write|MultiEdit|Agent|Task';
11348
12003
  migrated = true;
11349
12004
  }
11350
12005
  for (const h of entry.hooks) {
11351
- if (h.command && h.command.includes('gsd-context-monitor') && !h.timeout) {
12006
+ if (referencesHook(h, 'gsd-context-monitor') && !h.timeout) {
11352
12007
  h.timeout = 10;
11353
12008
  migrated = true;
11354
12009
  }
@@ -11368,7 +12023,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11368
12023
  }
11369
12024
 
11370
12025
  const hasPromptGuardHook = settings.hooks[preToolEvent].some(entry =>
11371
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-prompt-guard'))
12026
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-prompt-guard'))
11372
12027
  );
11373
12028
 
11374
12029
  const promptGuardFile = path.join(targetDir, 'hooks', 'gsd-prompt-guard.js');
@@ -11392,7 +12047,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11392
12047
  // Prevents infinite retry loops when non-Claude models attempt to edit
11393
12048
  // files without reading them first. Advisory-only — does not block.
11394
12049
  const hasReadGuardHook = settings.hooks[preToolEvent].some(entry =>
11395
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-read-guard'))
12050
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-read-guard'))
11396
12051
  );
11397
12052
 
11398
12053
  const readGuardFile = path.join(targetDir, 'hooks', 'gsd-read-guard.js');
@@ -11416,7 +12071,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11416
12071
  // Scans content returned by the Read tool for injection patterns, including
11417
12072
  // summarisation-specific patterns that survive context compression.
11418
12073
  const hasReadInjectionScannerHook = settings.hooks[postToolEvent].some(entry =>
11419
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-read-injection-scanner'))
12074
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-read-injection-scanner'))
11420
12075
  );
11421
12076
 
11422
12077
  const readInjectionScannerFile = path.join(targetDir, 'hooks', 'gsd-read-injection-scanner.js');
@@ -11450,7 +12105,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11450
12105
  : localCmd('gsd-workflow-guard.js');
11451
12106
  const workflowGuardMatcher = 'Bash|Edit|Write|MultiEdit';
11452
12107
  const workflowGuardHookEntry = settings.hooks[preToolEvent].find(entry =>
11453
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-workflow-guard'))
12108
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-workflow-guard'))
11454
12109
  );
11455
12110
  const hasWorkflowGuardHook = Boolean(workflowGuardHookEntry);
11456
12111
 
@@ -11482,7 +12137,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11482
12137
  ? buildHookCommand(targetDir, 'gsd-worktree-path-guard.js', hookOpts)
11483
12138
  : localCmd('gsd-worktree-path-guard.js');
11484
12139
  const hasWorktreePathGuardHook = settings.hooks[preToolEvent].some(entry =>
11485
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-worktree-path-guard'))
12140
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-worktree-path-guard'))
11486
12141
  );
11487
12142
  const worktreePathGuardFile = path.join(targetDir, 'hooks', 'gsd-worktree-path-guard.js');
11488
12143
  if (!hasWorktreePathGuardHook && fs.existsSync(worktreePathGuardFile) && worktreePathGuardCommand) {
@@ -11506,7 +12161,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11506
12161
  ? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
11507
12162
  : localShellCmd('gsd-validate-commit.sh');
11508
12163
  const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
11509
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-validate-commit'))
12164
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-validate-commit'))
11510
12165
  );
11511
12166
  // Guard: only register if the .sh file was actually installed. If the npm package
11512
12167
  // omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
@@ -11538,7 +12193,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11538
12193
  ? buildHookCommand(targetDir, 'gsd-graphify-update.sh', hookOpts)
11539
12194
  : localShellCmd('gsd-graphify-update.sh');
11540
12195
  const hasGraphifyUpdateHook = settings.hooks[postToolEvent].some(entry =>
11541
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-graphify-update'))
12196
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-graphify-update'))
11542
12197
  );
11543
12198
  const graphifyUpdateFile = path.join(targetDir, 'hooks', 'gsd-graphify-update.sh');
11544
12199
  if (!hasGraphifyUpdateHook && fs.existsSync(graphifyUpdateFile) && graphifyUpdateCommand) {
@@ -11564,7 +12219,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11564
12219
  ? buildHookCommand(targetDir, 'gsd-session-state.sh', hookOpts)
11565
12220
  : localShellCmd('gsd-session-state.sh');
11566
12221
  const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
11567
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-session-state'))
12222
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-session-state'))
11568
12223
  );
11569
12224
  const sessionStateFile = path.join(targetDir, 'hooks', 'gsd-session-state.sh');
11570
12225
  if (!hasSessionStateHook && fs.existsSync(sessionStateFile) && sessionStateCommand) {
@@ -11588,7 +12243,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11588
12243
  ? buildHookCommand(targetDir, 'gsd-phase-boundary.sh', hookOpts)
11589
12244
  : localShellCmd('gsd-phase-boundary.sh');
11590
12245
  const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
11591
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-phase-boundary'))
12246
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-phase-boundary'))
11592
12247
  );
11593
12248
  const phaseBoundaryFile = path.join(targetDir, 'hooks', 'gsd-phase-boundary.sh');
11594
12249
  if (!hasPhaseBoundaryHook && fs.existsSync(phaseBoundaryFile) && phaseBoundaryCommand) {
@@ -11632,7 +12287,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11632
12287
  settings.hooks[event] = [];
11633
12288
  }
11634
12289
  const alreadyHasContextMonitor = settings.hooks[event].some(entry =>
11635
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))
12290
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-context-monitor'))
11636
12291
  );
11637
12292
  if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) {
11638
12293
  settings.hooks[event].push({
@@ -11681,7 +12336,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11681
12336
  settings.hooks[geminiEvent] = [];
11682
12337
  }
11683
12338
  const alreadyHasContextMonitor = settings.hooks[geminiEvent].some(entry =>
11684
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))
12339
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-context-monitor'))
11685
12340
  );
11686
12341
  if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) {
11687
12342
  settings.hooks[geminiEvent].push({
@@ -11718,7 +12373,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11718
12373
  }
11719
12374
  const configReloadFile = path.join(targetDir, 'hooks', 'gsd-config-reload.js');
11720
12375
  const alreadyHasConfigReload = settings.hooks.FileChanged.some(entry =>
11721
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-config-reload'))
12376
+ entry.hooks && entry.hooks.some(h => referencesHook(h, 'gsd-config-reload'))
11722
12377
  );
11723
12378
  if (!alreadyHasConfigReload && fs.existsSync(configReloadFile) && configReloadCommand) {
11724
12379
  settings.hooks.FileChanged.push({
@@ -11856,7 +12511,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11856
12511
  const isCline = runtime === 'cline';
11857
12512
  const configIntent = resolveRuntimeConfigIntent(runtime);
11858
12513
 
11859
- if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
12514
+ if (shouldInstallStatusline && configIntent.writesSharedSettings && !isOpencode) {
11860
12515
  if (!isGlobal && !forceStatusline) {
11861
12516
  // Local installs skip statusLine by default: repo settings.json takes precedence over
11862
12517
  // profile-level settings.json in Claude Code, so writing here would silently clobber
@@ -11882,14 +12537,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11882
12537
  // settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
11883
12538
  // cline either lack the surface or use a different config schema.
11884
12539
  const { shouldInstallBanner, bannerCommand } = bannerOpts;
11885
- if (shouldInstallBanner && settings && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
12540
+ if (shouldInstallBanner && settings && configIntent.writesSharedSettings && !isOpencode) {
11886
12541
  if (!bannerCommand) {
11887
12542
  console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
11888
12543
  } else {
11889
12544
  if (!settings.hooks) settings.hooks = {};
11890
12545
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
11891
12546
  const alreadyRegistered = settings.hooks.SessionStart.some(entry =>
11892
- entry && entry.hooks && entry.hooks.some(h => h && h.command && h.command.includes('gsd-update-banner'))
12547
+ entry && entry.hooks && entry.hooks.some(h => h && referencesHook(h, 'gsd-update-banner'))
11893
12548
  );
11894
12549
  const bannerHookFile = configDir ? path.join(configDir, 'hooks', 'gsd-update-banner.js') : null;
11895
12550
  const bannerInstalled = bannerHookFile ? fs.existsSync(bannerHookFile) : false;
@@ -11922,7 +12577,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11922
12577
  // {type: 'command', command: null} items that the runtime hook schema
11923
12578
  // rejects at parse time. validateHookFields filters those out so the file
11924
12579
  // we write is always schema-valid.
11925
- if (configIntent.writesSharedSettings) {
12580
+ if (settingsPath && settings && configIntent.writesSharedSettings) {
11926
12581
  writeSettings(settingsPath, validateHookFields(settings));
11927
12582
  }
11928
12583
 
@@ -11971,6 +12626,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11971
12626
  if (runtime === 'cline') program = 'Cline';
11972
12627
  if (runtime === 'qwen') program = 'Qwen Code';
11973
12628
  if (runtime === 'hermes') program = 'Hermes Agent';
12629
+ if (runtime === 'kimi') program = 'Kimi CLI';
11974
12630
 
11975
12631
  let command = '/gsd-new-project';
11976
12632
  if (runtime === 'opencode') command = '/gsd-new-project';
@@ -11986,6 +12642,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11986
12642
  if (runtime === 'cline') command = '/gsd-new-project';
11987
12643
  if (runtime === 'qwen') command = '/gsd-new-project';
11988
12644
  if (runtime === 'hermes') command = '/gsd-new-project';
12645
+ if (runtime === 'kimi') command = '/skill:gsd-new-project';
11989
12646
 
11990
12647
  // Claude Code global installs use the skills/ format (CC 2.1.88+).
11991
12648
  // Restart is required for CC to pick up newly-installed skills, and the
@@ -12000,6 +12657,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
12000
12657
  return;
12001
12658
  }
12002
12659
 
12660
+ if (runtime === 'kimi') {
12661
+ const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml';
12662
+ console.log(`
12663
+ ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}.
12664
+
12665
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
12666
+ `);
12667
+ return;
12668
+ }
12669
+
12003
12670
  console.log(`
12004
12671
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
12005
12672
 
@@ -12076,14 +12743,15 @@ const runtimeMap = {
12076
12743
  '8': 'cursor',
12077
12744
  '9': 'gemini',
12078
12745
  '10': 'hermes',
12079
- '11': 'kilo',
12080
- '12': 'opencode',
12081
- '13': 'qwen',
12082
- '14': 'trae',
12083
- '15': 'windsurf'
12746
+ '11': 'kimi',
12747
+ '12': 'kilo',
12748
+ '13': 'opencode',
12749
+ '14': 'qwen',
12750
+ '15': 'trae',
12751
+ '16': 'windsurf'
12084
12752
  };
12085
- const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
12086
- const ALL_RUNTIMES_OPTION = '16';
12753
+ const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
12754
+ const ALL_RUNTIMES_OPTION = '17';
12087
12755
 
12088
12756
  /**
12089
12757
  * Build the runtime-selection prompt text shown by the interactive installer.
@@ -12101,12 +12769,13 @@ function buildRuntimePromptText() {
12101
12769
  ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
12102
12770
  ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset}
12103
12771
  ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset}
12104
- ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset}
12105
- ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
12106
- ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset}
12107
- ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset}
12108
- ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
12109
- ${cyan}16${reset}) All
12772
+ ${cyan}11${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset}
12773
+ ${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset}
12774
+ ${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
12775
+ ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset}
12776
+ ${cyan}15${reset}) Trae ${dim}(~/.trae)${reset}
12777
+ ${cyan}16${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
12778
+ ${cyan}17${reset}) All
12110
12779
 
12111
12780
  ${dim}Select multiple: 1,2,6 or 1 2 6${reset}
12112
12781
  `;
@@ -12117,7 +12786,7 @@ function buildRuntimePromptText() {
12117
12786
  * Pure function — exported so tests can verify split/dedupe/fallback behavior.
12118
12787
  * - Accepts comma- and/or whitespace-separated choices
12119
12788
  * - Deduplicates while preserving order
12120
- * - Maps option 16 ("All") to every runtime
12789
+ * - Maps option 17 ("All") to every runtime
12121
12790
  * - Falls back to ['claude'] when nothing valid is selected
12122
12791
  */
12123
12792
  function parseRuntimeInput(answer) {
@@ -12569,6 +13238,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
12569
13238
  try {
12570
13239
  const printSummaries = () => {
12571
13240
  for (const result of results) {
13241
+ if (result && result.skipped) continue;
13242
+ if (!result) continue;
12572
13243
  const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
12573
13244
  finishInstall(
12574
13245
  result.settingsPath,
@@ -12682,6 +13353,10 @@ module.exports = {
12682
13353
  uninstall,
12683
13354
  convertSlashCommandsToCodexSkillMentions,
12684
13355
  convertClaudeCommandToCodexSkill,
13356
+ convertClaudeCommandToKimiSkill,
13357
+ convertKimiToolName,
13358
+ mapClaudeToolsToKimiTools,
13359
+ buildKimiAgentArtifacts,
12685
13360
  convertClaudeToOpencodeFrontmatter,
12686
13361
  convertClaudeToKiloFrontmatter,
12687
13362
  convertClaudeCommandToOpencodeSkill,
@@ -12695,6 +13370,7 @@ module.exports = {
12695
13370
  GSD_CODEX_MARKER,
12696
13371
  CODEX_AGENT_SANDBOX,
12697
13372
  getDirName,
13373
+ getGlobalDir,
12698
13374
  getConfigDirFromHome,
12699
13375
  resolveKiloConfigPath,
12700
13376
  configureKiloPermissions,
@@ -12761,6 +13437,7 @@ module.exports = {
12761
13437
  maybeSuggestPathExport,
12762
13438
  runtimeMap,
12763
13439
  allRuntimes,
13440
+ selectRuntimesFromArgs,
12764
13441
  GSD_UNINSTALL_HOOKS,
12765
13442
  parseRuntimeInput,
12766
13443
  buildRuntimePromptText,
@@ -12811,12 +13488,11 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
12811
13488
  console.error('Usage: node install.js --skills-root <runtime>');
12812
13489
  process.exit(1);
12813
13490
  }
12814
- const globalDir = getGlobalConfigDir(runtimeArg, null);
12815
- // Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
12816
- // Other runtimes use a flat skills/ root.
12817
- const skillsRoot = runtimeArg === 'hermes'
12818
- ? path.join(globalDir, 'skills', 'gsd')
12819
- : path.join(globalDir, 'skills');
13491
+ const skillsRoot = getGlobalSkillsBase(runtimeArg);
13492
+ if (skillsRoot === null) {
13493
+ console.error(`${runtimeArg} does not use a skills directory`);
13494
+ process.exit(1);
13495
+ }
12820
13496
  console.log(skillsRoot);
12821
13497
  } else if (hasGlobal && hasLocal) {
12822
13498
  console.error(` ${yellow}Cannot specify both --global and --local${reset}`);