@opengsd/gsd-core 1.4.2 → 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 (153) 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 +947 -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 +6 -5
  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/installer-migrations/004-prune-stale-pristine-snapshots.cjs +123 -0
  30. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  31. package/gsd-core/bin/lib/io.cjs +167 -0
  32. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  33. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  34. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  35. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  36. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  37. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  38. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  39. package/gsd-core/bin/lib/roadmap.cjs +4 -1
  40. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
  41. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
  42. package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
  43. package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
  44. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  45. package/gsd-core/bin/lib/state.cjs +114 -2
  46. package/gsd-core/bin/lib/surface.cjs +66 -14
  47. package/gsd-core/bin/lib/verify.cjs +1 -1
  48. package/gsd-core/bin/shared/model-catalog.json +5 -0
  49. package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
  50. package/gsd-core/bin/verify-reapply-patches.cjs +57 -7
  51. package/gsd-core/references/planner-reviews.md +5 -2
  52. package/gsd-core/templates/phase-prompt.md +7 -7
  53. package/gsd-core/templates/project.md +19 -2
  54. package/gsd-core/templates/summary-complex.md +1 -0
  55. package/gsd-core/templates/summary-minimal.md +1 -0
  56. package/gsd-core/templates/summary-standard.md +1 -0
  57. package/gsd-core/templates/summary.md +1 -0
  58. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  59. package/gsd-core/workflows/add-backlog.md +1 -1
  60. package/gsd-core/workflows/add-phase.md +1 -1
  61. package/gsd-core/workflows/add-tests.md +1 -1
  62. package/gsd-core/workflows/add-todo.md +1 -1
  63. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  64. package/gsd-core/workflows/audit-fix.md +1 -1
  65. package/gsd-core/workflows/audit-milestone.md +1 -1
  66. package/gsd-core/workflows/audit-uat.md +1 -1
  67. package/gsd-core/workflows/autonomous.md +86 -15
  68. package/gsd-core/workflows/check-todos.md +1 -1
  69. package/gsd-core/workflows/cleanup.md +1 -1
  70. package/gsd-core/workflows/code-review-fix.md +6 -4
  71. package/gsd-core/workflows/code-review.md +3 -2
  72. package/gsd-core/workflows/complete-milestone.md +11 -5
  73. package/gsd-core/workflows/debug.md +1 -1
  74. package/gsd-core/workflows/diagnose-issues.md +1 -1
  75. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  76. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  77. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  78. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  79. package/gsd-core/workflows/discuss-phase.md +8 -1
  80. package/gsd-core/workflows/do.md +1 -1
  81. package/gsd-core/workflows/docs-update.md +1 -1
  82. package/gsd-core/workflows/edit-phase.md +1 -1
  83. package/gsd-core/workflows/eval-review.md +4 -1
  84. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  85. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  86. package/gsd-core/workflows/execute-phase.md +8 -1
  87. package/gsd-core/workflows/execute-plan.md +1 -1
  88. package/gsd-core/workflows/explore.md +1 -1
  89. package/gsd-core/workflows/extract-learnings.md +1 -1
  90. package/gsd-core/workflows/forensics.md +1 -1
  91. package/gsd-core/workflows/graduation.md +1 -1
  92. package/gsd-core/workflows/health.md +1 -1
  93. package/gsd-core/workflows/help/modes/full.md +1 -1
  94. package/gsd-core/workflows/import.md +1 -1
  95. package/gsd-core/workflows/ingest-docs.md +1 -1
  96. package/gsd-core/workflows/insert-phase.md +1 -1
  97. package/gsd-core/workflows/list-workspaces.md +1 -1
  98. package/gsd-core/workflows/manager.md +1 -1
  99. package/gsd-core/workflows/map-codebase.md +1 -1
  100. package/gsd-core/workflows/milestone-summary.md +1 -1
  101. package/gsd-core/workflows/mvp-phase.md +1 -1
  102. package/gsd-core/workflows/new-milestone.md +1 -1
  103. package/gsd-core/workflows/new-project.md +1 -1
  104. package/gsd-core/workflows/new-workspace.md +1 -1
  105. package/gsd-core/workflows/next.md +1 -1
  106. package/gsd-core/workflows/pause-work.md +1 -1
  107. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  108. package/gsd-core/workflows/plan-phase.md +27 -2
  109. package/gsd-core/workflows/plan-review-convergence.md +76 -63
  110. package/gsd-core/workflows/plant-seed.md +1 -1
  111. package/gsd-core/workflows/profile-user.md +1 -1
  112. package/gsd-core/workflows/progress.md +1 -1
  113. package/gsd-core/workflows/quick.md +2 -2
  114. package/gsd-core/workflows/reapply-patches.md +18 -1
  115. package/gsd-core/workflows/remove-phase.md +1 -1
  116. package/gsd-core/workflows/remove-workspace.md +1 -1
  117. package/gsd-core/workflows/resume-project.md +1 -1
  118. package/gsd-core/workflows/review.md +1 -1
  119. package/gsd-core/workflows/scan.md +1 -1
  120. package/gsd-core/workflows/secure-phase.md +1 -1
  121. package/gsd-core/workflows/settings-advanced.md +1 -1
  122. package/gsd-core/workflows/settings-integrations.md +1 -1
  123. package/gsd-core/workflows/settings.md +1 -1
  124. package/gsd-core/workflows/ship.md +8 -1
  125. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  126. package/gsd-core/workflows/sketch.md +1 -1
  127. package/gsd-core/workflows/spec-phase.md +1 -1
  128. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  129. package/gsd-core/workflows/spike.md +1 -1
  130. package/gsd-core/workflows/stats.md +1 -1
  131. package/gsd-core/workflows/thread.md +1 -1
  132. package/gsd-core/workflows/transition.md +1 -1
  133. package/gsd-core/workflows/ui-phase.md +1 -1
  134. package/gsd-core/workflows/ui-review.md +1 -1
  135. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  136. package/gsd-core/workflows/update.md +20 -15
  137. package/gsd-core/workflows/validate-phase.md +1 -1
  138. package/gsd-core/workflows/verify-phase.md +1 -1
  139. package/gsd-core/workflows/verify-work.md +8 -1
  140. package/package.json +7 -2
  141. package/scripts/base64-scan.sh +1 -1
  142. package/scripts/changeset/cli.cjs +9 -2
  143. package/scripts/changeset/lint.cjs +38 -2
  144. package/scripts/ci-test-scope.cjs +21 -10
  145. package/scripts/gen-capability-registry.cjs +1808 -0
  146. package/scripts/gen-loop-host-contract.cjs +471 -0
  147. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  148. package/scripts/lint-regression-test-names.cjs +117 -0
  149. package/scripts/lint-test-file-count.allowlist.json +12 -4
  150. package/scripts/prompt-injection-scan.sh +4 -4
  151. package/scripts/run-tests.cjs +79 -16
  152. package/scripts/secret-scan.sh +3 -3
  153. 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`);
@@ -8496,6 +9070,52 @@ function uninstall(isGlobal, runtime = 'claude') {
8496
9070
  }
8497
9071
  }
8498
9072
 
9073
+ // 4a. Remove scripts/changeset/ and scripts/lib/ (#935)
9074
+ // GSD-managed files only: enumerate the exact set the installer writes.
9075
+ // Any file NOT in this set is user-owned and must survive uninstall.
9076
+ // After removing GSD files, attempt to rmdir — if the directory is still
9077
+ // non-empty (user has custom helpers) it stays; otherwise it goes cleanly.
9078
+ const GSD_CHANGESET_FILES = [
9079
+ 'cli.cjs', 'parse.cjs', 'render.cjs', 'serialize.cjs',
9080
+ 'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
9081
+ 'README.md', // documentation only — not user-authored
9082
+ ];
9083
+ const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs'];
9084
+
9085
+ const changesetUninstallDir = path.join(targetDir, 'scripts', 'changeset');
9086
+ if (fs.existsSync(changesetUninstallDir)) {
9087
+ let removedChangeset = 0;
9088
+ for (const file of GSD_CHANGESET_FILES) {
9089
+ const fp = path.join(changesetUninstallDir, file);
9090
+ try { fs.unlinkSync(fp); removedChangeset++; } catch (_) { /* best-effort */ }
9091
+ }
9092
+ // Remove directory if empty after our cleanup
9093
+ try { fs.rmdirSync(changesetUninstallDir); } catch (_) { /* Not empty — user content present */ }
9094
+ if (removedChangeset > 0) {
9095
+ removedCount++;
9096
+ console.log(` ${green}✓${reset} Removed scripts/changeset/ GSD files`);
9097
+ }
9098
+ }
9099
+ const scriptsLibUninstallDir = path.join(targetDir, 'scripts', 'lib');
9100
+ if (fs.existsSync(scriptsLibUninstallDir)) {
9101
+ let removedScriptsLib = 0;
9102
+ for (const file of GSD_SCRIPTS_LIB_FILES) {
9103
+ const fp = path.join(scriptsLibUninstallDir, file);
9104
+ try { fs.unlinkSync(fp); removedScriptsLib++; } catch (_) { /* best-effort */ }
9105
+ }
9106
+ // Remove directory if empty after our cleanup
9107
+ try { fs.rmdirSync(scriptsLibUninstallDir); } catch (_) { /* Not empty — user content present */ }
9108
+ if (removedScriptsLib > 0) {
9109
+ removedCount++;
9110
+ console.log(` ${green}✓${reset} Removed scripts/lib/ GSD files`);
9111
+ }
9112
+ }
9113
+ // If scripts/ dir is now empty, remove it too
9114
+ const scriptsUninstallDir = path.join(targetDir, 'scripts');
9115
+ if (fs.existsSync(scriptsUninstallDir)) {
9116
+ try { fs.rmdirSync(scriptsUninstallDir); } catch (_) { /* Not empty — leave it */ }
9117
+ }
9118
+
8499
9119
  // 5. Remove GSD package.json (CommonJS mode marker)
8500
9120
  const pkgJsonPath = path.join(targetDir, 'package.json');
8501
9121
  if (fs.existsSync(pkgJsonPath)) {
@@ -9063,6 +9683,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9063
9683
  const isWindsurf = runtime === 'windsurf';
9064
9684
  const isTrae = runtime === 'trae';
9065
9685
  const isCline = runtime === 'cline';
9686
+ const isKimi = runtime === 'kimi';
9066
9687
  const isHermes = runtime === 'hermes';
9067
9688
  const gsdDir = path.join(configDir, 'gsd-core');
9068
9689
  const commandsDir = path.join(configDir, 'commands', 'gsd');
@@ -9110,8 +9731,8 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9110
9731
  }
9111
9732
  }
9112
9733
  if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || isTrae || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
9113
- // Hermes uses prefix '' (bare stem names); all others use 'gsd-'
9114
- const skillListPrefix = isHermes ? '' : 'gsd-';
9734
+ // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
9735
+ const skillListPrefix = 'gsd-';
9115
9736
  for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
9116
9737
  const skillRoot = path.join(codexSkillsDir, skillName);
9117
9738
  const skillHashes = generateManifest(skillRoot);
@@ -9127,7 +9748,16 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9127
9748
  }
9128
9749
  }
9129
9750
  }
9130
- 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)) {
9131
9761
  for (const file of fs.readdirSync(agentsDir)) {
9132
9762
  if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
9133
9763
  manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
@@ -9148,12 +9778,20 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9148
9778
 
9149
9779
  // Track hook files so saveLocalPatches() can detect user modifications
9150
9780
  // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
9151
- if (!isCodex && !isCopilot && !isCline) {
9781
+ if (!isCodex && !isCopilot && !isCline && !isKimi) {
9152
9782
  const hooksDir = path.join(configDir, 'hooks');
9153
9783
  if (fs.existsSync(hooksDir)) {
9154
- for (const file of fs.readdirSync(hooksDir)) {
9155
- if (file.startsWith('gsd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
9156
- 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);
9157
9795
  }
9158
9796
  }
9159
9797
  // Track hooks/lib/ helpers so saveLocalPatches() can back up user edits
@@ -9169,6 +9807,24 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
9169
9807
  }
9170
9808
  }
9171
9809
 
9810
+ // Track scripts/changeset/ and scripts/lib/ so saveLocalPatches() can detect drift
9811
+ const changesetInstallDir = path.join(configDir, 'scripts', 'changeset');
9812
+ if (fs.existsSync(changesetInstallDir)) {
9813
+ for (const file of fs.readdirSync(changesetInstallDir)) {
9814
+ if (file.endsWith('.cjs')) {
9815
+ manifest.files['scripts/changeset/' + file] = fileHash(path.join(changesetInstallDir, file));
9816
+ }
9817
+ }
9818
+ }
9819
+ const scriptsLibInstallDir = path.join(configDir, 'scripts', 'lib');
9820
+ if (fs.existsSync(scriptsLibInstallDir)) {
9821
+ for (const file of fs.readdirSync(scriptsLibInstallDir)) {
9822
+ if (file.endsWith('.cjs')) {
9823
+ manifest.files['scripts/lib/' + file] = fileHash(path.join(scriptsLibInstallDir, file));
9824
+ }
9825
+ }
9826
+ }
9827
+
9172
9828
  fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
9173
9829
  return manifest;
9174
9830
  }
@@ -9466,6 +10122,8 @@ function reportLocalPatches(configDir, runtime = 'claude') {
9466
10122
  ? '$gsd-update --reapply'
9467
10123
  : runtime === 'cursor'
9468
10124
  ? 'gsd-update --reapply (mention the skill name)'
10125
+ : runtime === 'kimi'
10126
+ ? '/skill:gsd-update --reapply'
9469
10127
  : '/gsd-update --reapply';
9470
10128
  console.log('');
9471
10129
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
@@ -9496,6 +10154,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9496
10154
  const isOpencode = runtime === 'opencode';
9497
10155
  const isGemini = runtime === 'gemini';
9498
10156
  const isKilo = runtime === 'kilo';
10157
+ const isKimi = runtime === 'kimi';
9499
10158
  const isCodex = runtime === 'codex';
9500
10159
  const isCopilot = runtime === 'copilot';
9501
10160
  const isAntigravity = runtime === 'antigravity';
@@ -9511,6 +10170,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9511
10170
  const dirName = getDirName(runtime);
9512
10171
  const src = path.join(__dirname, '..');
9513
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
+
9514
10190
  // Reusable helper to copy hooks/lib/ (git-cmd.js + gsd-graphify-rebuild.sh).
9515
10191
  // Defined early so it is visible to both the main and Codex code paths.
9516
10192
  // `allowlist` (when non-empty) restricts copying to the named top-level entries,
@@ -9565,7 +10241,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9565
10241
  // @-references resolve correctly (#2376 Windows, #2831 macOS/Linux).
9566
10242
  // gsd update marker re-application (ADR-0010 Deviation 2):
9567
10243
  // Resolve which profile to use for this runtime's install:
9568
- // 1. --minimal / --core-only → back-compat path (stageSkillsForMode keeps strict core allowlist)
10244
+ // 1. --minimal / --core-only → back-compat alias for the core profile
9569
10245
  // 2. Explicit --profile=<name> → use it (overrides any marker)
9570
10246
  // 3. Marker exists in targetDir → honor it (prevents silent expansion on update)
9571
10247
  // 4. Else → 'full' (back-compat for fresh non-interactive installs)
@@ -9574,8 +10250,16 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9574
10250
  // differ, the caller may use mostRestrictiveProfile() across the per-runtime
9575
10251
  // results — here we resolve each runtime independently.
9576
10252
  //
9577
- // Note: --minimal uses stageSkillsForMode (back-compat: strict allowlist, no closure).
9578
- // 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.
9579
10263
  const _activeProfileName = hasMinimal
9580
10264
  ? 'core' // --minimal is a back-compat alias for the core profile; marker records 'core'
9581
10265
  : resolveEffectiveProfile({
@@ -9586,19 +10270,18 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9586
10270
  const _effectiveInstallMode = _isCoreProfileAlias ? 'minimal' : 'full';
9587
10271
  // Load the manifest and compute resolved profile for named profiles.
9588
10272
  // For --minimal/core: use an empty manifest (core profile has no transitive
9589
- // deps) to produce a resolvedProfile with the core skill set. This allows
9590
- // installRuntimeArtifacts to use stageSkillsForProfile uniformly across all
9591
- // 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.
9592
10275
  const _commandsDir = path.join(src, 'commands', 'gsd');
9593
10276
  const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
9594
10277
  const _resolvedProfile = resolveProfile({
9595
10278
  modes: [_activeProfileName],
9596
10279
  manifest: _skillsManifest,
10280
+ registry: _capabilityRegistry,
9597
10281
  });
9598
- // Unified staging function: for --minimal uses stageSkillsForMode (back-compat);
9599
- // 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).
9600
10284
  function _stageSkills(commandsGsdDir) {
9601
- if (_isCoreProfileAlias) return stageSkillsForMode(commandsGsdDir, _effectiveInstallMode);
9602
10285
  return stageSkillsForProfile(commandsGsdDir, _resolvedProfile);
9603
10286
  }
9604
10287
  function _stageAgents(agentsDir) {
@@ -9637,6 +10320,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9637
10320
  if (isTrae) runtimeLabel = 'Trae';
9638
10321
  if (isQwen) runtimeLabel = 'Qwen Code';
9639
10322
  if (isHermes) runtimeLabel = 'Hermes Agent';
10323
+ if (isKimi) runtimeLabel = 'Kimi';
9640
10324
  if (isCodebuddy) runtimeLabel = 'CodeBuddy';
9641
10325
  if (isCline) runtimeLabel = 'Cline';
9642
10326
 
@@ -9926,6 +10610,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9926
10610
  // Cline global: emit skills to ~/.cline/skills/ (Cline >= v3.48.0 — #782).
9927
10611
  const _isSkillsRuntime = isCodex || isCopilot || isAntigravity || isCursor || isWindsurf ||
9928
10612
  isAugment || isTrae || isCodebuddy || isQwen || isHermes ||
10613
+ isKimi ||
9929
10614
  (runtime === 'claude' && isGlobal) ||
9930
10615
  (isCline && isGlobal);
9931
10616
 
@@ -9953,9 +10638,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9953
10638
  if (isHermes) {
9954
10639
  const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
9955
10640
  if (fs.existsSync(hermesSkillsDir)) {
9956
- // 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
9957
10642
  const count = fs.readdirSync(hermesSkillsDir, { withFileTypes: true })
9958
- .filter(e => e.isDirectory()).length;
10643
+ .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
9959
10644
  if (count > 0) {
9960
10645
  console.log(` ${green}✓${reset} Installed ${count} skills to skills/gsd/`);
9961
10646
  } else {
@@ -9964,6 +10649,26 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9964
10649
  } else {
9965
10650
  failures.push('skills/gsd/*');
9966
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
+ }
9967
10672
  } else {
9968
10673
  const skillsDir = path.join(targetDir, 'skills');
9969
10674
  if (fs.existsSync(skillsDir)) {
@@ -10200,7 +10905,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10200
10905
  }
10201
10906
  }
10202
10907
 
10203
- 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)) {
10204
10911
  // Codex registers agents in `config.toml` via `[agents.gsd-*]` sections.
10205
10912
  // Without stripping them here, a full → minimal reinstall would leave the
10206
10913
  // runtime advertising the old full agent surface even though the agent
@@ -10343,7 +11050,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10343
11050
  failures.push('VERSION');
10344
11051
  }
10345
11052
 
10346
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
11053
+ if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) {
10347
11054
  // Write package.json to force CommonJS mode for GSD scripts
10348
11055
  // Prevents "require is not defined" errors when project has "type": "module"
10349
11056
  // Node.js walks up looking for package.json - this stops inheritance from project
@@ -10439,13 +11146,71 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10439
11146
  // the hooks/lib/ helpers — otherwise the Codex comment downstream
10440
11147
  // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice.
10441
11148
  const hooksLibSrc = path.join(src, 'hooks', 'lib');
10442
- if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && fs.existsSync(hooksLibSrc)) {
11149
+ if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) {
10443
11150
  const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
10444
11151
  fs.mkdirSync(hooksLibDest, { recursive: true });
10445
11152
  copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
10446
11153
  console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
10447
11154
  }
10448
11155
 
11156
+ // Install scripts/changeset/ and scripts/lib/ into <configDir>/scripts/
11157
+ // so that `node "$GSD_DIR/scripts/changeset/cli.cjs"` resolves at runtime.
11158
+ //
11159
+ // The changeset CLI (scripts/changeset/cli.cjs) is invoked by the update
11160
+ // workflow (gsd-core/workflows/update.md) to extract changelog ranges for
11161
+ // the /gsd-update preview step. It was previously only present in the npm
11162
+ // tarball root but never copied to the runtime config dir, causing the
11163
+ // preview to always silently fail (#935).
11164
+ //
11165
+ // cli.cjs requires:
11166
+ // - sibling files in scripts/changeset/ (parse/render/serialize/github-release-notes)
11167
+ // - ../lib/cli-exit.cjs → scripts/lib/cli-exit.cjs
11168
+ // - ../../gsd-core/bin/lib/semver-compare.cjs (already installed under gsd-core/)
11169
+ // - ../../gsd-core/bin/lib/package-identity.cjs (already installed under gsd-core/)
11170
+ //
11171
+ // All runtimes that use the update workflow need this, so we copy unconditionally
11172
+ // (same scope as gsd-core/ itself — every runtime that installs workflows gets it).
11173
+ const changesetSrc = path.join(src, 'scripts', 'changeset');
11174
+ const scriptsLibSrc = path.join(src, 'scripts', 'lib');
11175
+ if (!fs.existsSync(changesetSrc)) {
11176
+ // The changeset CLI source is missing from the package — mark as a hard failure
11177
+ // so the user knows the changelog preview will not work rather than silently degrading.
11178
+ failures.push('scripts/changeset/ (source missing from package — reinstall from npm)');
11179
+ } else {
11180
+ const changesetDest = path.join(targetDir, 'scripts', 'changeset');
11181
+ const scriptsLibDest = path.join(targetDir, 'scripts', 'lib');
11182
+ fs.mkdirSync(changesetDest, { recursive: true });
11183
+ fs.mkdirSync(scriptsLibDest, { recursive: true });
11184
+ // Copy scripts/changeset/ — all .cjs and .md files
11185
+ for (const entry of fs.readdirSync(changesetSrc)) {
11186
+ const srcFile = path.join(changesetSrc, entry);
11187
+ if (fs.statSync(srcFile).isFile()) {
11188
+ fs.copyFileSync(srcFile, path.join(changesetDest, entry));
11189
+ }
11190
+ }
11191
+ // Copy scripts/lib/ — cli-exit.cjs (required by cli.cjs) and any future lib helpers.
11192
+ // Hard-fail if missing: without cli-exit.cjs the installed CLI throws MODULE_NOT_FOUND.
11193
+ if (!fs.existsSync(scriptsLibSrc)) {
11194
+ failures.push('scripts/lib/ (source missing from package — reinstall from npm)');
11195
+ } else {
11196
+ for (const entry of fs.readdirSync(scriptsLibSrc)) {
11197
+ const srcFile = path.join(scriptsLibSrc, entry);
11198
+ if (fs.statSync(srcFile).isFile()) {
11199
+ fs.copyFileSync(srcFile, path.join(scriptsLibDest, entry));
11200
+ }
11201
+ }
11202
+ // Verify the critical dep cli-exit.cjs landed
11203
+ if (!verifyFileInstalled(path.join(scriptsLibDest, 'cli-exit.cjs'), 'scripts/lib/cli-exit.cjs')) {
11204
+ failures.push('scripts/lib/cli-exit.cjs');
11205
+ }
11206
+ }
11207
+ if (verifyFileInstalled(path.join(changesetDest, 'cli.cjs'), 'scripts/changeset/cli.cjs')) {
11208
+ console.log(` ${green}✓${reset} Installed scripts/changeset/ (changelog preview CLI)`);
11209
+ } else {
11210
+ failures.push('scripts/changeset/cli.cjs');
11211
+ }
11212
+ }
11213
+
10449
11214
  // Remove legacy get-shit-done-cc artifacts and stale update caches (#607).
10450
11215
  // cleanupLegacyGsdCc handles both the legacy shared cache and the per-package
10451
11216
  // cache (formerly an inline unlinkSync here). A cleanup failure must never
@@ -10967,7 +11732,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10967
11732
  }
10968
11733
 
10969
11734
  if (configIntent.installSurface === 'profile-marker-only') {
10970
- // 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.
10971
11736
  persistActiveProfileMarker();
10972
11737
  return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
10973
11738
  }
@@ -11160,6 +11925,18 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11160
11925
  }
11161
11926
  }
11162
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
+
11163
11940
  // Configure SessionStart hook for update checking (skip for opencode)
11164
11941
  if (!isOpencode && !isKilo) {
11165
11942
  if (!settings.hooks) {
@@ -11170,7 +11947,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11170
11947
  }
11171
11948
 
11172
11949
  const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
11173
- 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'))
11174
11951
  );
11175
11952
 
11176
11953
  // Guard: only register if the hook file was actually installed (#1754).
@@ -11198,7 +11975,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11198
11975
  }
11199
11976
 
11200
11977
  const hasContextMonitorHook = settings.hooks[postToolEvent].some(entry =>
11201
- 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'))
11202
11979
  );
11203
11980
 
11204
11981
  const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
@@ -11219,14 +11996,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11219
11996
  } else {
11220
11997
  // Migrate existing context monitor hooks: add matcher and timeout if missing
11221
11998
  for (const entry of settings.hooks[postToolEvent]) {
11222
- 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'))) {
11223
12000
  let migrated = false;
11224
12001
  if (!entry.matcher) {
11225
12002
  entry.matcher = 'Bash|Edit|Write|MultiEdit|Agent|Task';
11226
12003
  migrated = true;
11227
12004
  }
11228
12005
  for (const h of entry.hooks) {
11229
- if (h.command && h.command.includes('gsd-context-monitor') && !h.timeout) {
12006
+ if (referencesHook(h, 'gsd-context-monitor') && !h.timeout) {
11230
12007
  h.timeout = 10;
11231
12008
  migrated = true;
11232
12009
  }
@@ -11246,7 +12023,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11246
12023
  }
11247
12024
 
11248
12025
  const hasPromptGuardHook = settings.hooks[preToolEvent].some(entry =>
11249
- 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'))
11250
12027
  );
11251
12028
 
11252
12029
  const promptGuardFile = path.join(targetDir, 'hooks', 'gsd-prompt-guard.js');
@@ -11270,7 +12047,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11270
12047
  // Prevents infinite retry loops when non-Claude models attempt to edit
11271
12048
  // files without reading them first. Advisory-only — does not block.
11272
12049
  const hasReadGuardHook = settings.hooks[preToolEvent].some(entry =>
11273
- 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'))
11274
12051
  );
11275
12052
 
11276
12053
  const readGuardFile = path.join(targetDir, 'hooks', 'gsd-read-guard.js');
@@ -11294,7 +12071,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11294
12071
  // Scans content returned by the Read tool for injection patterns, including
11295
12072
  // summarisation-specific patterns that survive context compression.
11296
12073
  const hasReadInjectionScannerHook = settings.hooks[postToolEvent].some(entry =>
11297
- 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'))
11298
12075
  );
11299
12076
 
11300
12077
  const readInjectionScannerFile = path.join(targetDir, 'hooks', 'gsd-read-injection-scanner.js');
@@ -11328,7 +12105,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11328
12105
  : localCmd('gsd-workflow-guard.js');
11329
12106
  const workflowGuardMatcher = 'Bash|Edit|Write|MultiEdit';
11330
12107
  const workflowGuardHookEntry = settings.hooks[preToolEvent].find(entry =>
11331
- 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'))
11332
12109
  );
11333
12110
  const hasWorkflowGuardHook = Boolean(workflowGuardHookEntry);
11334
12111
 
@@ -11360,7 +12137,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11360
12137
  ? buildHookCommand(targetDir, 'gsd-worktree-path-guard.js', hookOpts)
11361
12138
  : localCmd('gsd-worktree-path-guard.js');
11362
12139
  const hasWorktreePathGuardHook = settings.hooks[preToolEvent].some(entry =>
11363
- 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'))
11364
12141
  );
11365
12142
  const worktreePathGuardFile = path.join(targetDir, 'hooks', 'gsd-worktree-path-guard.js');
11366
12143
  if (!hasWorktreePathGuardHook && fs.existsSync(worktreePathGuardFile) && worktreePathGuardCommand) {
@@ -11384,7 +12161,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11384
12161
  ? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
11385
12162
  : localShellCmd('gsd-validate-commit.sh');
11386
12163
  const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
11387
- 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'))
11388
12165
  );
11389
12166
  // Guard: only register if the .sh file was actually installed. If the npm package
11390
12167
  // omitted the file (as happened in v1.32.0, bug #1817), registering a missing hook
@@ -11416,7 +12193,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11416
12193
  ? buildHookCommand(targetDir, 'gsd-graphify-update.sh', hookOpts)
11417
12194
  : localShellCmd('gsd-graphify-update.sh');
11418
12195
  const hasGraphifyUpdateHook = settings.hooks[postToolEvent].some(entry =>
11419
- 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'))
11420
12197
  );
11421
12198
  const graphifyUpdateFile = path.join(targetDir, 'hooks', 'gsd-graphify-update.sh');
11422
12199
  if (!hasGraphifyUpdateHook && fs.existsSync(graphifyUpdateFile) && graphifyUpdateCommand) {
@@ -11442,7 +12219,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11442
12219
  ? buildHookCommand(targetDir, 'gsd-session-state.sh', hookOpts)
11443
12220
  : localShellCmd('gsd-session-state.sh');
11444
12221
  const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
11445
- 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'))
11446
12223
  );
11447
12224
  const sessionStateFile = path.join(targetDir, 'hooks', 'gsd-session-state.sh');
11448
12225
  if (!hasSessionStateHook && fs.existsSync(sessionStateFile) && sessionStateCommand) {
@@ -11466,7 +12243,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11466
12243
  ? buildHookCommand(targetDir, 'gsd-phase-boundary.sh', hookOpts)
11467
12244
  : localShellCmd('gsd-phase-boundary.sh');
11468
12245
  const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
11469
- 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'))
11470
12247
  );
11471
12248
  const phaseBoundaryFile = path.join(targetDir, 'hooks', 'gsd-phase-boundary.sh');
11472
12249
  if (!hasPhaseBoundaryHook && fs.existsSync(phaseBoundaryFile) && phaseBoundaryCommand) {
@@ -11510,7 +12287,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11510
12287
  settings.hooks[event] = [];
11511
12288
  }
11512
12289
  const alreadyHasContextMonitor = settings.hooks[event].some(entry =>
11513
- 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'))
11514
12291
  );
11515
12292
  if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) {
11516
12293
  settings.hooks[event].push({
@@ -11559,7 +12336,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11559
12336
  settings.hooks[geminiEvent] = [];
11560
12337
  }
11561
12338
  const alreadyHasContextMonitor = settings.hooks[geminiEvent].some(entry =>
11562
- 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'))
11563
12340
  );
11564
12341
  if (!alreadyHasContextMonitor && fs.existsSync(contextMonitorFile) && contextMonitorCommand) {
11565
12342
  settings.hooks[geminiEvent].push({
@@ -11596,7 +12373,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11596
12373
  }
11597
12374
  const configReloadFile = path.join(targetDir, 'hooks', 'gsd-config-reload.js');
11598
12375
  const alreadyHasConfigReload = settings.hooks.FileChanged.some(entry =>
11599
- 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'))
11600
12377
  );
11601
12378
  if (!alreadyHasConfigReload && fs.existsSync(configReloadFile) && configReloadCommand) {
11602
12379
  settings.hooks.FileChanged.push({
@@ -11734,7 +12511,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11734
12511
  const isCline = runtime === 'cline';
11735
12512
  const configIntent = resolveRuntimeConfigIntent(runtime);
11736
12513
 
11737
- if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
12514
+ if (shouldInstallStatusline && configIntent.writesSharedSettings && !isOpencode) {
11738
12515
  if (!isGlobal && !forceStatusline) {
11739
12516
  // Local installs skip statusLine by default: repo settings.json takes precedence over
11740
12517
  // profile-level settings.json in Claude Code, so writing here would silently clobber
@@ -11760,14 +12537,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11760
12537
  // settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
11761
12538
  // cline either lack the surface or use a different config schema.
11762
12539
  const { shouldInstallBanner, bannerCommand } = bannerOpts;
11763
- if (shouldInstallBanner && settings && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) {
12540
+ if (shouldInstallBanner && settings && configIntent.writesSharedSettings && !isOpencode) {
11764
12541
  if (!bannerCommand) {
11765
12542
  console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
11766
12543
  } else {
11767
12544
  if (!settings.hooks) settings.hooks = {};
11768
12545
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
11769
12546
  const alreadyRegistered = settings.hooks.SessionStart.some(entry =>
11770
- 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'))
11771
12548
  );
11772
12549
  const bannerHookFile = configDir ? path.join(configDir, 'hooks', 'gsd-update-banner.js') : null;
11773
12550
  const bannerInstalled = bannerHookFile ? fs.existsSync(bannerHookFile) : false;
@@ -11800,7 +12577,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11800
12577
  // {type: 'command', command: null} items that the runtime hook schema
11801
12578
  // rejects at parse time. validateHookFields filters those out so the file
11802
12579
  // we write is always schema-valid.
11803
- if (configIntent.writesSharedSettings) {
12580
+ if (settingsPath && settings && configIntent.writesSharedSettings) {
11804
12581
  writeSettings(settingsPath, validateHookFields(settings));
11805
12582
  }
11806
12583
 
@@ -11849,6 +12626,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11849
12626
  if (runtime === 'cline') program = 'Cline';
11850
12627
  if (runtime === 'qwen') program = 'Qwen Code';
11851
12628
  if (runtime === 'hermes') program = 'Hermes Agent';
12629
+ if (runtime === 'kimi') program = 'Kimi CLI';
11852
12630
 
11853
12631
  let command = '/gsd-new-project';
11854
12632
  if (runtime === 'opencode') command = '/gsd-new-project';
@@ -11864,6 +12642,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11864
12642
  if (runtime === 'cline') command = '/gsd-new-project';
11865
12643
  if (runtime === 'qwen') command = '/gsd-new-project';
11866
12644
  if (runtime === 'hermes') command = '/gsd-new-project';
12645
+ if (runtime === 'kimi') command = '/skill:gsd-new-project';
11867
12646
 
11868
12647
  // Claude Code global installs use the skills/ format (CC 2.1.88+).
11869
12648
  // Restart is required for CC to pick up newly-installed skills, and the
@@ -11878,6 +12657,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11878
12657
  return;
11879
12658
  }
11880
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
+
11881
12670
  console.log(`
11882
12671
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
11883
12672
 
@@ -11954,14 +12743,15 @@ const runtimeMap = {
11954
12743
  '8': 'cursor',
11955
12744
  '9': 'gemini',
11956
12745
  '10': 'hermes',
11957
- '11': 'kilo',
11958
- '12': 'opencode',
11959
- '13': 'qwen',
11960
- '14': 'trae',
11961
- '15': 'windsurf'
12746
+ '11': 'kimi',
12747
+ '12': 'kilo',
12748
+ '13': 'opencode',
12749
+ '14': 'qwen',
12750
+ '15': 'trae',
12751
+ '16': 'windsurf'
11962
12752
  };
11963
- const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'gemini', 'hermes', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf'];
11964
- 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';
11965
12755
 
11966
12756
  /**
11967
12757
  * Build the runtime-selection prompt text shown by the interactive installer.
@@ -11979,12 +12769,13 @@ function buildRuntimePromptText() {
11979
12769
  ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset}
11980
12770
  ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset}
11981
12771
  ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset}
11982
- ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset}
11983
- ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
11984
- ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset}
11985
- ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset}
11986
- ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
11987
- ${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
11988
12779
 
11989
12780
  ${dim}Select multiple: 1,2,6 or 1 2 6${reset}
11990
12781
  `;
@@ -11995,7 +12786,7 @@ function buildRuntimePromptText() {
11995
12786
  * Pure function — exported so tests can verify split/dedupe/fallback behavior.
11996
12787
  * - Accepts comma- and/or whitespace-separated choices
11997
12788
  * - Deduplicates while preserving order
11998
- * - Maps option 16 ("All") to every runtime
12789
+ * - Maps option 17 ("All") to every runtime
11999
12790
  * - Falls back to ['claude'] when nothing valid is selected
12000
12791
  */
12001
12792
  function parseRuntimeInput(answer) {
@@ -12447,6 +13238,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
12447
13238
  try {
12448
13239
  const printSummaries = () => {
12449
13240
  for (const result of results) {
13241
+ if (result && result.skipped) continue;
13242
+ if (!result) continue;
12450
13243
  const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
12451
13244
  finishInstall(
12452
13245
  result.settingsPath,
@@ -12560,6 +13353,10 @@ module.exports = {
12560
13353
  uninstall,
12561
13354
  convertSlashCommandsToCodexSkillMentions,
12562
13355
  convertClaudeCommandToCodexSkill,
13356
+ convertClaudeCommandToKimiSkill,
13357
+ convertKimiToolName,
13358
+ mapClaudeToolsToKimiTools,
13359
+ buildKimiAgentArtifacts,
12563
13360
  convertClaudeToOpencodeFrontmatter,
12564
13361
  convertClaudeToKiloFrontmatter,
12565
13362
  convertClaudeCommandToOpencodeSkill,
@@ -12573,6 +13370,7 @@ module.exports = {
12573
13370
  GSD_CODEX_MARKER,
12574
13371
  CODEX_AGENT_SANDBOX,
12575
13372
  getDirName,
13373
+ getGlobalDir,
12576
13374
  getConfigDirFromHome,
12577
13375
  resolveKiloConfigPath,
12578
13376
  configureKiloPermissions,
@@ -12639,6 +13437,7 @@ module.exports = {
12639
13437
  maybeSuggestPathExport,
12640
13438
  runtimeMap,
12641
13439
  allRuntimes,
13440
+ selectRuntimesFromArgs,
12642
13441
  GSD_UNINSTALL_HOOKS,
12643
13442
  parseRuntimeInput,
12644
13443
  buildRuntimePromptText,
@@ -12689,12 +13488,11 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
12689
13488
  console.error('Usage: node install.js --skills-root <runtime>');
12690
13489
  process.exit(1);
12691
13490
  }
12692
- const globalDir = getGlobalConfigDir(runtimeArg, null);
12693
- // Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
12694
- // Other runtimes use a flat skills/ root.
12695
- const skillsRoot = runtimeArg === 'hermes'
12696
- ? path.join(globalDir, 'skills', 'gsd')
12697
- : 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
+ }
12698
13496
  console.log(skillsRoot);
12699
13497
  } else if (hasGlobal && hasLocal) {
12700
13498
  console.error(` ${yellow}Cannot specify both --global and --local${reset}`);