@opengsd/gsd-core 1.9.1 → 1.10.0

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 (219) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +453 -289
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +579 -66
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +75 -47
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -28
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  62. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  63. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  64. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  67. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  68. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  69. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  70. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  71. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  72. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  73. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  74. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  75. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  76. package/gsd-core/bin/lib/state.cjs +151 -62
  77. package/gsd-core/bin/lib/surface.cjs +12 -1
  78. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  79. package/gsd-core/bin/lib/uat.cjs +320 -21
  80. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  81. package/gsd-core/bin/lib/verification.cjs +29 -12
  82. package/gsd-core/bin/lib/verify.cjs +10 -2
  83. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  84. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  85. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  86. package/gsd-core/bin/lib/workstream.cjs +6 -0
  87. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  88. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  89. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  90. package/gsd-core/references/artifact-types.md +10 -3
  91. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  92. package/gsd-core/references/debugger-techniques.md +255 -0
  93. package/gsd-core/references/research-documentation-lookup.md +5 -3
  94. package/gsd-core/references/specless-probe-fallback.md +7 -6
  95. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  96. package/gsd-core/references/worktree-branch-check.md +2 -2
  97. package/gsd-core/templates/summary-complex.md +2 -0
  98. package/gsd-core/templates/summary-minimal.md +2 -0
  99. package/gsd-core/templates/summary-standard.md +2 -0
  100. package/gsd-core/templates/summary.md +2 -0
  101. package/gsd-core/workflows/audit-milestone.md +3 -0
  102. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  107. package/gsd-core/workflows/autonomous.md +32 -69
  108. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  109. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  110. package/gsd-core/workflows/code-review.md +42 -160
  111. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  112. package/gsd-core/workflows/complete-milestone.md +23 -81
  113. package/gsd-core/workflows/debug.md +9 -12
  114. package/gsd-core/workflows/diagnose-issues.md +22 -0
  115. package/gsd-core/workflows/discovery-phase.md +4 -4
  116. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  117. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  118. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  119. package/gsd-core/workflows/docs-update.md +8 -51
  120. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  121. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  122. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  124. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  126. package/gsd-core/workflows/execute-phase.md +65 -137
  127. package/gsd-core/workflows/execute-plan.md +1 -1
  128. package/gsd-core/workflows/help/modes/full.md +6 -1
  129. package/gsd-core/workflows/ingest-docs.md +2 -1
  130. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  131. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  132. package/gsd-core/workflows/new-milestone.md +21 -38
  133. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  134. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  135. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  136. package/gsd-core/workflows/new-project.md +13 -226
  137. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  138. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  139. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  140. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  142. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  143. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  144. package/gsd-core/workflows/plan-phase.md +49 -193
  145. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  146. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  147. package/gsd-core/workflows/progress.md +11 -153
  148. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  149. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  150. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  151. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  152. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  153. package/gsd-core/workflows/quick.md +20 -390
  154. package/gsd-core/workflows/resume-project.md +3 -0
  155. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  157. package/gsd-core/workflows/review.md +15 -8
  158. package/gsd-core/workflows/section-manifest.json +219 -0
  159. package/gsd-core/workflows/sketch.md +1 -1
  160. package/gsd-core/workflows/spec-phase.md +17 -14
  161. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  162. package/gsd-core/workflows/spike.md +50 -16
  163. package/gsd-core/workflows/sync-skills.md +49 -11
  164. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  165. package/gsd-core/workflows/transition.md +8 -21
  166. package/gsd-core/workflows/ui-phase.md +8 -7
  167. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  168. package/gsd-core/workflows/update.md +18 -7
  169. package/gsd-core/workflows/verify-phase.md +4 -7
  170. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  171. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  172. package/gsd-core/workflows/verify-work.md +8 -58
  173. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  174. package/hooks/dist/gsd-check-update-worker.js +14 -5
  175. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  176. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  177. package/hooks/dist/gsd-statusline.js +72 -6
  178. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  179. package/hooks/dist/gsd-write-guard.js +359 -0
  180. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  181. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  182. package/hooks/gsd-agent-isolation-guard.js +428 -0
  183. package/hooks/gsd-check-update-worker.js +14 -5
  184. package/hooks/gsd-cursor-subagent-start.js +532 -26
  185. package/hooks/gsd-read-injection-scanner.js +7 -0
  186. package/hooks/gsd-statusline.js +72 -6
  187. package/hooks/gsd-worktree-path-guard.js +2 -1
  188. package/hooks/gsd-write-guard.js +359 -0
  189. package/hooks/hooks.json +12 -0
  190. package/hooks/lib/isolation-sentinel.js +268 -0
  191. package/hooks/managed-hooks-registry.cjs +2 -0
  192. package/package.json +14 -5
  193. package/pi/gsd.cjs +57 -12
  194. package/scripts/build-hooks.js +9 -0
  195. package/scripts/changeset/lint.cjs +9 -2
  196. package/scripts/changeset/serialize.cjs +5 -1
  197. package/scripts/gen-capability-matrix.cjs +1 -1
  198. package/scripts/gen-context-index.cjs +448 -0
  199. package/scripts/gen-inventory-manifest.cjs +101 -1
  200. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  201. package/scripts/gen-section-manifest.cjs +638 -0
  202. package/scripts/generate-package-identity.cjs +4 -2
  203. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  204. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  205. package/scripts/lint-docs-command-form.cjs +195 -0
  206. package/scripts/lint-docs-required.cjs +9 -1
  207. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  208. package/scripts/lint-example-parser-parity.cjs +395 -0
  209. package/scripts/lint-test-file-count.allowlist.json +27 -1
  210. package/scripts/mutation-matrix.cjs +13 -0
  211. package/scripts/prompt-injection-scan.sh +27 -6
  212. package/scripts/run-tests.cjs +3 -2
  213. package/skills/gsd-autonomous/SKILL.md +1 -1
  214. package/skills/gsd-execute-phase/SKILL.md +1 -1
  215. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  216. package/skills/gsd-new-milestone/SKILL.md +1 -1
  217. package/skills/gsd-plan-phase/SKILL.md +2 -2
  218. package/vscode/package.json +1 -1
  219. package/scripts/gen-emitted-baseline.cjs +0 -145
package/bin/install.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  projectPathActionProjection,
15
15
  projectPortableHookBaseDir,
16
16
  projectPersistentPathExportActions,
17
+ PATH_ACTION_REASON,
17
18
  projectShellCommandText,
18
19
  projectCodexHookTomlCommand,
19
20
  shellHookOmitsBashRunner,
@@ -33,6 +34,7 @@ const {
33
34
  getGlobalConfigDir,
34
35
  getGlobalSkillsBase,
35
36
  resolveKimiHooksTomlDir,
37
+ isRegisteredRuntimeId,
36
38
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
37
39
  // getDirName (runtime -> local config dir name) is relocated out of this
38
40
  // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
@@ -45,7 +47,18 @@ const {
45
47
  } = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
46
48
  const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
47
49
  const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
50
+ // #2930 (epic #1671 Phase 3): strips `<!-- gsd:section -->` markers from
51
+ // workflow .md content at emit time, before any per-runtime rewrite runs.
52
+ const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
53
+ // #3072: THE shared composition-scope predicate (also consumed by the served
54
+ // MCP catalog, src/mcp-catalog.cts) — see the comment at its call site below.
55
+ const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
48
56
  const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
57
+ // #2544: the CommonJS marker's single source of truth. classifyMarker() backs
58
+ // BOTH ensureCommonJsMarker() (install) and removeCommonJsMarker() (uninstall),
59
+ // so the write side can no longer clobber a package.json the remove side would
60
+ // correctly refuse to delete.
61
+ const { ensureCommonJsMarker, removeCommonJsMarker } = require('../gsd-core/bin/lib/commonjs-marker.cjs');
49
62
  // Canonical set of hook files shipped to users. Imported here so writeManifest()
50
63
  // records exactly the same set that build-hooks.js copies to hooks/dist/, making
51
64
  // the manifest and the installed hooks/ dir structurally identical. Avoids the
@@ -333,6 +346,68 @@ const GSD_WINDSURF_HOOK_SCRIPTS = [
333
346
  // that does receive hooks/lib.
334
347
  const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js'];
335
348
 
349
+ /**
350
+ * Directory name GSD stages its shared hook bundle under, inside a runtime's
351
+ * install root. Defaults to 'hooks' — the name every runtime used before #3023.
352
+ *
353
+ * pi (pi.dev) reserves `hooks/` as its own now-deprecated extension location and
354
+ * prints a migration warning on every startup when one exists, so pi overrides
355
+ * this via hostBehaviors.sharedHooksDirName. Following pi's advised remediation
356
+ * (move it into extensions/) would break the adapter's path resolution AND expose
357
+ * GSD's .js helpers to pi's extension auto-discovery, so the bundle is renamed in
358
+ * place instead — same depth, so every `__dirname/..`-relative resolution inside
359
+ * the bundle (e.g. hooks/gsd-context-monitor.js reaching ../gsd-core/bin/) keeps
360
+ * working.
361
+ */
362
+ const SHARED_HOOKS_DIR_DEFAULT = 'hooks';
363
+
364
+ /**
365
+ * Resolve a runtime's shared-hooks directory name from its descriptor.
366
+ *
367
+ * The value is a single path SEGMENT. This string is joined onto a user's config
368
+ * root and then written to and recursively read, so anything that is not a plain,
369
+ * non-empty, separator-free, non-dot segment is rejected back to the default —
370
+ * a descriptor typo must never let the installer write outside the install root.
371
+ *
372
+ * The "non-dot" part of that contract is enforced beyond the literal '.' / '..'
373
+ * segments: an all-dot (or dot-and-whitespace-only) segment is rejected as a
374
+ * meaningless name, a segment with a trailing dot or space is rejected because
375
+ * Windows silently strips it at directory-creation time (which would split the
376
+ * name the installer creates from the name callers probe for), and a Windows
377
+ * reserved device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9, with or without an
378
+ * extension) is rejected because it cannot exist as a directory on Windows at
379
+ * all. These checks are unconditional on every platform: the descriptor is
380
+ * authored once and shipped everywhere, so a value invalid on Windows must be
381
+ * rejected identically on Linux/macOS, or the install and its fixtures disagree
382
+ * cross-platform.
383
+ *
384
+ * @param {string} runtime
385
+ * @returns {string}
386
+ */
387
+ function resolveSharedHooksDirName(runtime) {
388
+ const raw = _hostBehaviors(runtime).sharedHooksDirName;
389
+ if (typeof raw !== 'string') return SHARED_HOOKS_DIR_DEFAULT;
390
+ const name = raw.trim();
391
+ if (name === '') return SHARED_HOOKS_DIR_DEFAULT;
392
+ if (name === '.' || name === '..') return SHARED_HOOKS_DIR_DEFAULT;
393
+ // All-dot or dot+whitespace segments ('...', '. .') are not meaningful
394
+ // directory names and are almost certainly a descriptor typo.
395
+ if (name.replace(/[.\s]/g, '') === '') return SHARED_HOOKS_DIR_DEFAULT;
396
+ // Windows silently strips a trailing dot or space at creation time, so the
397
+ // directory the installer creates would not match the name the adapter
398
+ // probes for — a split-brain that only reproduces off-Linux.
399
+ if (/[. ]$/.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
400
+ // Windows reserved device names cannot exist as directories.
401
+ if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i.test(name)) return SHARED_HOOKS_DIR_DEFAULT;
402
+ if (name.includes('/') || name.includes('\\')) return SHARED_HOOKS_DIR_DEFAULT;
403
+ // Belt-and-braces: reject anything path.basename() would reduce, and any
404
+ // Windows drive/UNC-flavoured value.
405
+ if (path.basename(name) !== name) return SHARED_HOOKS_DIR_DEFAULT;
406
+ if (path.isAbsolute(name)) return SHARED_HOOKS_DIR_DEFAULT;
407
+ if (name.includes('\0')) return SHARED_HOOKS_DIR_DEFAULT;
408
+ return name;
409
+ }
410
+
336
411
  const CODEX_AGENT_SANDBOX = {
337
412
  'gsd-executor': 'workspace-write',
338
413
  'gsd-planner': 'workspace-write',
@@ -902,7 +977,7 @@ if (hasUninstall) {
902
977
 
903
978
  // Show help if requested
904
979
  if (hasHelp) {
905
- 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}--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}--zcode${reset} Install for ZCode 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 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 / 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`);
980
+ 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}--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}--zcode${reset} Install for ZCode only\n ${cyan}--pi${reset} Install for Pi only\n ${cyan}--gemini${reset} Install for Gemini CLI 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 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 / 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`);
906
981
  process.exit(0);
907
982
  }
908
983
 
@@ -952,6 +1027,31 @@ const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.
952
1027
  const convertClaudeToAugmentMarkdown = runtimeArtifactConversion.convertClaudeToAugmentMarkdown;
953
1028
  const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill;
954
1029
  const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent;
1030
+ // #2931 (ADR-1508): the windsurf converter family is single-sourced in the
1031
+ // conversion module, same pattern as the #1675 Augment dedup above. install.js
1032
+ // re-binds (does not re-define) these so there is exactly one body — the
1033
+ // generative-drift hazard the dedup removes. The two private helpers
1034
+ // (getWindsurfSkillAdapterHeader, convertSlashCommandsToWindsurfSkillMentions)
1035
+ // live only in the conversion module now; they are no longer duplicated here.
1036
+ // The reference-identity parity guard lives in
1037
+ // tests/install-runtime-artifacts.test.cjs (single-owner reference-identity
1038
+ // guard describe block), not tests/enh-1511-rewrite-engine-relocation.test.cjs
1039
+ // as the Augment comment above stated — that reference was stale.
1040
+ // (All call sites are below this line → no TDZ hazard.)
1041
+ const convertClaudeToWindsurfMarkdown = runtimeArtifactConversion.convertClaudeToWindsurfMarkdown;
1042
+ const convertClaudeCommandToWindsurfSkill = runtimeArtifactConversion.convertClaudeCommandToWindsurfSkill;
1043
+ const convertClaudeCommandToWindsurfWorkflow = runtimeArtifactConversion.convertClaudeCommandToWindsurfWorkflow;
1044
+ const convertClaudeAgentToWindsurfAgent = runtimeArtifactConversion.convertClaudeAgentToWindsurfAgent;
1045
+ // #2931 (ADR-1508): single-sourced in the conversion module — was a second,
1046
+ // unlinked verbatim copy here (used by the local Cursor/Trae/CodeBuddy/Cline
1047
+ // converters below), the exact drift class this PR exists to reduce. Verified
1048
+ // behaviorally identical (no block / one block / adjacent blocks / whole-
1049
+ // content block / unclosed opening tag / nested-looking tags / repeated
1050
+ // sequential calls for global-regex lastIndex leakage) before merging.
1051
+ // install.js re-binds (does not re-define) — RUNTIME_COMPATIBILITY_BLOCK_RE
1052
+ // is no longer duplicated here either. (All call sites are below this line
1053
+ // → no TDZ hazard.)
1054
+ const applyClaudeCodeBrandSwap = runtimeArtifactConversion.applyClaudeCodeBrandSwap;
955
1055
 
956
1056
  function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
957
1057
  return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
@@ -1947,7 +2047,9 @@ function skillFrontmatterName(skillDirName) {
1947
2047
  }
1948
2048
 
1949
2049
  function normalizeClaudeSkillEffort(effort) {
1950
- return effort === 'xhigh' ? 'max' : effort;
2050
+ // #3039: `max` is rejected by Anthropic models when extended thinking is disabled.
2051
+ if (effort === 'xhigh' || effort === 'max') return 'high';
2052
+ return effort;
1951
2053
  }
1952
2054
 
1953
2055
  /**
@@ -2501,56 +2603,6 @@ function extractFrontmatterField(frontmatter, fieldName) {
2501
2603
  return match[1].trim().replace(/^['"]|['"]$/g, '');
2502
2604
  }
2503
2605
 
2504
- // #2284 finding (b): the `<runtime_compatibility>` block appearing in
2505
- // gsd-core/workflows/{plan-phase,execute-phase}.md is a runtime-COMPARISON
2506
- // table ("**Claude Code:** Uses `Agent(...)`" / "a backgrounded Claude Code
2507
- // agent" / "top-level Claude Code") — every "Claude Code" mention inside it
2508
- // is a COMPARED-RUNTIME LABEL, not a host self-reference. The brand swap
2509
- // below (`Claude Code` → the installing runtime's own display name) is
2510
- // meant only for host self-references; applying it inside this block
2511
- // mislabels the comparison (e.g. Windsurf installs would read "**Windsurf:**
2512
- // Uses `Agent(...)`" describing what is actually Claude Code's behavior).
2513
- // This is cross-cutting across every runtime that brand-swaps workflow
2514
- // content (cursor/windsurf/trae/cline/codebuddy hardcoded; qwen/hermes
2515
- // descriptor-driven via hostBehaviors.brandingRewrites) — confirmed to
2516
- // reproduce on unmodified Windsurf, not Hermes-specific.
2517
- const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
2518
-
2519
- /**
2520
- * Rewrite bare "Claude Code" self-references in workflow content to
2521
- * `brandName`, EXCEPT inside `<runtime_compatibility>...</runtime_compatibility>`
2522
- * blocks, which are left byte-for-byte verbatim. Every other content
2523
- * transform in a runtime's `.md` converter (tool-name renames, path
2524
- * rewrites, etc.) is unaffected — only this literal brand-name swap is
2525
- * protected-region-aware, since only it risks mislabeling a
2526
- * runtime-comparison table.
2527
- *
2528
- * Implementation: SPLIT `content` on the protected-block regex, brand-swap
2529
- * only the GAP text between (and around) matches, then rejoin gap+block
2530
- * alternately. No placeholder/sentinel token of any kind is substituted in
2531
- * — a prior version used a sentinel-token mask/restore, which is exactly the
2532
- * kind of invisible landmine this rewrite eliminates (a sentinel string, no
2533
- * matter how obscure, is a theoretical collision risk with real content and
2534
- * is easy to silently reintroduce in a future edit without it showing in a
2535
- * diff). Behavior-identical to the removed sentinel-token version — verified
2536
- * via `npm run gen:golden` producing zero further diff.
2537
- */
2538
- function applyClaudeCodeBrandSwap(content, brandName) {
2539
- if (!brandName) return content;
2540
- let result = '';
2541
- let lastIndex = 0;
2542
- RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
2543
- let m;
2544
- while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
2545
- const gap = content.slice(lastIndex, m.index);
2546
- result += gap.replace(/\bClaude Code\b/g, brandName);
2547
- result += m[0]; // protected block, verbatim — never brand-swapped
2548
- lastIndex = m.index + m[0].length;
2549
- }
2550
- result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
2551
- return result;
2552
- }
2553
-
2554
2606
  // Tool name mapping from Claude Code to Cursor CLI
2555
2607
  const claudeToCursorTools = {
2556
2608
  Bash: 'Shell',
@@ -2629,36 +2681,11 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
2629
2681
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
2630
2682
  const adapter = getCursorSkillAdapterHeader(skillName);
2631
2683
 
2632
- // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
2633
- // menu (it defaults to true). Cursor also writes a commands/ surface (#785),
2634
- // and surfacing both duplicated every /gsd-* entry. This mirrors the #789
2635
- // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
2636
- // stay model-invocable background knowledge. (user-invocable:false hides from
2637
- // '/' while keeping model invocation — distinct from disable-model-invocation.)
2638
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
2639
- }
2640
-
2641
- /**
2642
- * Convert a Claude Code command to a Cursor 1.6 slash command (#785).
2643
- *
2644
- * Cursor slash commands live in `.cursor/commands/<name>.md` and are
2645
- * plain markdown — no YAML frontmatter, no adapter header. The filename
2646
- * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
2647
- *
2648
- * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
2649
- * converter (tool renames, brand substitution, slash-command normalisation),
2650
- * then strips the YAML frontmatter block so only the prose body remains.
2651
- *
2652
- * @param {string} content raw Claude Code command markdown (may have frontmatter)
2653
- * @param {string} _commandName the target command name (unused; present for
2654
- * API symmetry with other converters so the runtime-artifact-layout stage
2655
- * function can call it uniformly)
2656
- * @returns {string} plain markdown body, no frontmatter
2657
- */
2658
- function convertClaudeCommandToCursorCommand(content, _commandName) {
2659
- const converted = convertClaudeToCursorMarkdown(content);
2660
- const { body } = extractFrontmatterAndBody(converted);
2661
- return body.trimStart();
2684
+ // Cursor skills are both slash-invocable and model-invocable. Do not emit the
2685
+ // unsupported `user-invocable` field: it is ignored by Cursor and previously
2686
+ // hid the real cause of duplicate entries, the parallel commands/ surface
2687
+ // retired in #2644.
2688
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
2662
2689
  }
2663
2690
 
2664
2691
  /**
@@ -2681,142 +2708,16 @@ function convertClaudeAgentToCursorAgent(content) {
2681
2708
  }
2682
2709
 
2683
2710
  // --- Windsurf converters ---
2684
- // Windsurf uses a tool set similar to Cursor.
2685
- // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
2686
-
2687
- // Tool name mapping from Claude Code to Windsurf Cascade
2688
- const claudeToWindsurfTools = {
2689
- Bash: 'Shell',
2690
- Edit: 'StrReplace',
2691
- AskUserQuestion: null, // No direct equivalent — use conversational prompting
2692
- SlashCommand: null, // No equivalent — skills are auto-discovered
2693
- };
2694
-
2695
- function convertSlashCommandsToWindsurfSkillMentions(content) {
2696
- // Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
2697
- return content.replace(/gsd:/gi, 'gsd-');
2698
- }
2699
-
2700
- function convertClaudeToWindsurfMarkdown(content) {
2701
- let converted = convertSlashCommandsToWindsurfSkillMentions(content);
2702
- // Replace tool name references in body text
2703
- converted = converted.replace(/\bBash\(/g, 'Shell(');
2704
- converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
2705
- converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
2706
- // Replace subagent_type from Claude to Windsurf format
2707
- converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
2708
- converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
2709
- // Replace project-level Claude conventions with Windsurf equivalents.
2710
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
2711
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
2712
- converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
2713
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
2714
- converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
2715
- converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
2716
- converted = converted.replace(/\.claude\//g, '.windsurf/');
2717
- // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
2718
- // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
2719
- converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
2720
- converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
2721
- // Environment variable name rewrite
2722
- converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
2723
- // Remove Claude Code-specific bug workarounds before brand replacement
2724
- converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
2725
- converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
2726
- // Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
2727
- // <runtime_compatibility> comparison-table content (protected region).
2728
- converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
2729
- return converted;
2730
- }
2731
-
2732
- function getWindsurfSkillAdapterHeader(skillName) {
2733
- return `<windsurf_skill_adapter>
2734
- ## A. Skill Invocation
2735
- - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
2736
- - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
2737
- - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
2738
-
2739
- ## B. User Prompting
2740
- When the workflow needs user input, prompt the user conversationally:
2741
- - Present options as a numbered list in your response text
2742
- - Ask the user to reply with their choice
2743
- - For multi-select, ask for comma-separated numbers
2744
-
2745
- ## C. Tool Usage
2746
- Use these Windsurf tools when executing GSD workflows:
2747
- - \`Shell\` for running commands (terminal operations)
2748
- - \`StrReplace\` for editing existing files
2749
- - \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
2750
-
2751
- ## D. Subagent Spawning
2752
- When the workflow needs to spawn a subagent:
2753
- - Use \`Task(subagent_type="generalPurpose", ...)\`
2754
- - The \`model\` parameter maps to Windsurf's model options (e.g., "fast")
2755
- </windsurf_skill_adapter>`;
2756
- }
2757
-
2758
- function convertClaudeCommandToWindsurfSkill(content, skillName) {
2759
- const converted = convertClaudeToWindsurfMarkdown(content);
2760
- const { frontmatter, body } = extractFrontmatterAndBody(converted);
2761
- let description = `Run GSD workflow ${skillName}.`;
2762
- if (frontmatter) {
2763
- const maybeDescription = extractFrontmatterField(frontmatter, 'description');
2764
- if (maybeDescription) {
2765
- description = maybeDescription;
2766
- }
2767
- }
2768
- description = toSingleLine(description);
2769
- const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
2770
- const adapter = getWindsurfSkillAdapterHeader(skillName);
2771
-
2772
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
2773
- }
2774
-
2775
- function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
2776
- // #1615 security: commandName flows unsanitized into a markdown body that
2777
- // Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
2778
- // (a) prompt injection via newlines / markdown structure in the filename,
2779
- // (b) path-component injection via .., /, \ in stem → @-reference target.
2780
- // Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
2781
- // everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
2782
- // PR #1622 security review.
2783
- if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
2784
- const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
2785
- throw new Error(
2786
- `convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
2787
- 'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
2788
- );
2789
- }
2790
- const converted = convertClaudeToWindsurfMarkdown(content);
2791
- const { frontmatter } = extractFrontmatterAndBody(converted);
2792
- const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
2793
- const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
2794
- const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
2795
- const byteLength = Buffer.byteLength(workflow, 'utf8');
2796
- if (byteLength > 12000) {
2797
- throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
2798
- }
2799
- return workflow;
2800
- }
2801
-
2802
- /**
2803
- * Convert Claude Code agent markdown to Windsurf agent format.
2804
- * Strips frontmatter fields Windsurf doesn't support (color, skills),
2805
- * converts tool references, and adds a role context header.
2806
- */
2807
- function convertClaudeAgentToWindsurfAgent(content) {
2808
- let converted = convertClaudeToWindsurfMarkdown(content);
2809
-
2810
- const { frontmatter, body } = extractFrontmatterAndBody(converted);
2811
- if (!frontmatter) return converted;
2812
-
2813
- const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2814
- const description = extractFrontmatterField(frontmatter, 'description') || '';
2815
-
2816
- const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
2817
-
2818
- return `${cleanFrontmatter}\n${body}`;
2819
- }
2711
+ // #2931 (ADR-1508): single-sourced in runtimeArtifactConversion, bound near
2712
+ // the top of this file alongside the #1675 Augment family. This block
2713
+ // previously carried byte-identical local duplicates of
2714
+ // convertSlashCommandsToWindsurfSkillMentions, convertClaudeToWindsurfMarkdown,
2715
+ // getWindsurfSkillAdapterHeader, convertClaudeCommandToWindsurfSkill,
2716
+ // convertClaudeCommandToWindsurfWorkflow, and convertClaudeAgentToWindsurfAgent,
2717
+ // plus an unused claudeToWindsurfTools table. Deleted here; the two
2718
+ // unexported helpers (getWindsurfSkillAdapterHeader,
2719
+ // convertSlashCommandsToWindsurfSkillMentions) now live only in the
2720
+ // conversion module, with no other caller in this file.
2820
2721
 
2821
2722
  // --- Augment converters ---
2822
2723
  // Augment uses a tool set similar to Cursor/Windsurf.
@@ -2844,10 +2745,55 @@ function convertClaudeToTraeMarkdown(content) {
2844
2745
  // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
2845
2746
  converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
2846
2747
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
2847
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
2848
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
2849
- converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
2850
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
2748
+ // #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
2749
+ // replaced before the bare Claude-instruction-file pattern and before the
2750
+ // generic dot-claude-slash rewrite below — otherwise the bare pattern
2751
+ // consumes only the instruction-filename tail, leaving that prefix stale
2752
+ // in place, and the generic rewrite then mutates the stale leftover too,
2753
+ // producing a doubled trae-prefix segment ahead of the rules path instead
2754
+ // of a single clean one. (Deliberately never spelling the instruction
2755
+ // filename as one contiguous "CLAUDE" + dot + "md" token, and never
2756
+ // spelling either malformed shape out as a literal contiguous string, in
2757
+ // ANY comment in this function: this file ships verbatim into local
2758
+ // `--trae` installs, where it is itself run through this same class of
2759
+ // find/replace — a literal instruction-filename token sitting in a
2760
+ // comment gets "fixed" right along with real code, and the emitted-content
2761
+ // regression test added alongside this fix asserts neither malformed
2762
+ // shape appears anywhere in the installed tree, comments included; this
2763
+ // bit the fix itself twice during development.) All forms converge on the
2764
+ // same concrete file (never a bare directory) so this stays in parity
2765
+ // with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
2766
+ converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
2767
+ converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
2768
+ converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
2769
+ converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
2770
+ // #2658 (found via the end-to-end install regression test, not the static
2771
+ // trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
2772
+ // -> runtime-config-dir rewrite on every .md file before calling this
2773
+ // converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
2774
+ // substituting a runtime-appropriate `pathPrefix` this function is never
2775
+ // given and cannot itself compute (it differs per install invocation: a
2776
+ // relative `./.trae/` for a project-local install, an arbitrary absolute
2777
+ // path for a local install rooted elsewhere, `~/.trae/` for a global one).
2778
+ // So for source using any of those prefixed forms, the patterns above
2779
+ // never fire here — this converter only ever sees the ALREADY-rewritten
2780
+ // "<runtime-config-dir>/" + instruction-filename shape, with whatever
2781
+ // prefix the install actually used. The generic pattern below preserves
2782
+ // that prefix verbatim (via the capture group) and only fixes the
2783
+ // filename suffix, rather than assuming a fixed `./.trae/` shape — a
2784
+ // narrower fixed-prefix version of this pattern shipped first and still
2785
+ // left the doubled-prefix defect live for the `$HOME/.claude/` and
2786
+ // `~/.claude/` forms specifically (found the same way, one regression-test
2787
+ // run later). Scoped to a `.trae/` tail so it cannot also swallow the
2788
+ // unprefixed `./CLAUDE.md` form the very next pattern handles differently
2789
+ // (discarding the prefix entirely, not preserving it). Must run before
2790
+ // the bare pattern for the same consume-the-full-match-first reason.
2791
+ converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
2792
+ converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
2793
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
2794
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
2795
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
2796
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
2851
2797
  converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
2852
2798
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
2853
2799
  converted = converted.replace(/\.claude\//g, '.trae/');
@@ -4019,6 +3965,8 @@ Typed mapping (agent_type-capable schema only):
4019
3965
  inherited, or unsupported values; do not invent one-off effort literals in
4020
3966
  workflow prose.
4021
3967
  - \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
3968
+ - \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
3969
+ - \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
4022
3970
  - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
4023
3971
  but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
4024
3972
  \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
@@ -4055,11 +4003,13 @@ Spawn restriction:
4055
4003
  defaulting to inline execution.
4056
4004
 
4057
4005
  Parallel fan-out:
4058
- - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
4006
+ - Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
4007
+ - Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
4059
4008
 
4060
4009
  Result parsing:
4061
4010
  - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
4062
- - \`close_agent(id)\` after collecting results from each agent
4011
+ - \`close_agent(id)\` after collecting results — but only if \`close_agent\` is visible in the current
4012
+ tool schema (check via \`tool_search\` first, same schema-detection gate as \`spawn_agent\` above)
4063
4013
  </codex_skill_adapter>`;
4064
4014
  }
4065
4015
 
@@ -6445,17 +6395,40 @@ function mergeCodexConfig(configPath, gsdBlock) {
6445
6395
  const normalizedGsdBlock = mergedGsdBlock.replace(/\r?\n/g, eol);
6446
6396
  const markerIndex = existing.indexOf(GSD_CODEX_MARKER);
6447
6397
 
6448
- // Case 2: Has GSD marker — truncate and re-append
6398
+ // Case 2: Has GSD marker — preserve user content on BOTH sides, regenerate the GSD block.
6399
+ //
6400
+ // #2940: the marker delimits where GSD's OWN block begins, NOT where every post-marker byte
6401
+ // is GSD-owned. A fresh install writes the GSD block as the file's entire content, so any
6402
+ // settings the user or Codex CLI later adds ([model], [mcp_servers.*], [profiles.*]) land
6403
+ // AFTER the block. The previous truncate-to-marker logic discarded that trailing region on
6404
+ // every update, destroying user config. The fix routes the trailing region through the
6405
+ // existing AST-based `stripLeakedGsdCodexSections`, which removes GSD's own managed/leaked
6406
+ // sections (the bare [agents] table GSD regenerates, legacy [agents.gsd-*], [[agents]])
6407
+ // while preserving genuine user TOML — so #2406's de-dup still holds AND user content survives.
6449
6408
  if (markerIndex !== -1) {
6450
6409
  let before = existing.substring(0, markerIndex).trimEnd();
6451
6410
  if (before) {
6452
6411
  // Strip any GSD-managed sections that leaked above the marker from previous installs
6453
6412
  before = stripLeakedGsdCodexSections(before).trimEnd();
6454
-
6455
- atomicWriteFileSync(configPath, before + eol + eol + normalizedGsdBlock + eol);
6456
- } else {
6457
- atomicWriteFileSync(configPath, normalizedGsdBlock + eol);
6458
6413
  }
6414
+ // Capture and preserve genuine user content AFTER the GSD-managed region. The whole
6415
+ // post-marker region is passed through stripLeakedGsdCodexSections: GSD's own previously-
6416
+ // emitted [agents] table (regenerated above as normalizedGsdBlock) and any leaked sections
6417
+ // are removed, while user tables ([model], [mcp_servers.*], [profiles.*]) are kept. The
6418
+ // marker comment line itself (and the optional codex_hooks ownership line right under it)
6419
+ // is GSD-owned and is stripped from the trailing region so it is not duplicated alongside
6420
+ // the freshly regenerated block.
6421
+ const rawAfter = existing.substring(markerIndex);
6422
+ const markerStripped = rawAfter
6423
+ .replace(GSD_CODEX_MARKER, '')
6424
+ .replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
6425
+ const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
6426
+
6427
+ const parts = [];
6428
+ if (before) parts.push(before);
6429
+ parts.push(normalizedGsdBlock);
6430
+ if (afterUser) parts.push(afterUser);
6431
+ atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
6459
6432
  return;
6460
6433
  }
6461
6434
 
@@ -7051,7 +7024,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7051
7024
  const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`;
7052
7025
 
7053
7026
  for (const file of agentEntries) {
7054
- let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
7027
+ const agentTomlSourcePath = path.join(agentsSrc, file);
7028
+ let content = fs.readFileSync(agentTomlSourcePath, 'utf8');
7029
+ // #2995 (epic #1671 Phase 6.4): Codex embeds each agent's prompt into a
7030
+ // per-agent `.toml`, reading the source .md independently of the inline
7031
+ // agent loop — a separate emission path that must strip gsd:section
7032
+ // markers too, or a marked agent ships its markers inside the TOML.
7033
+ // Found by the exhaustive per-runtime emission sweep in
7034
+ // tests/agent-fragments-emission.install.test.cjs, not by call-graph
7035
+ // analysis, which is why that guard is behavioral rather than structural.
7036
+ content = composeWorkflow(content, { sourcePath: agentTomlSourcePath });
7055
7037
  // Replace full .claude/gsd-core prefix so path resolves to the Codex
7056
7038
  // GSD install before generic .claude → .codex conversion rewrites it.
7057
7039
  content = content.replace(/~\/\.claude\/gsd-core\//g, codexGsdPath);
@@ -7668,7 +7650,18 @@ const RUNTIME_CONTENT_DISPATCH = {
7668
7650
  return `/gsd-${commandName}`;
7669
7651
  });
7670
7652
  content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
7671
- content = content.replace(/CLAUDE\.md/g, '.trae/rules/');
7653
+ // #2658: the full dot-claude-slash-prefixed instruction-file path must
7654
+ // be replaced before the bare instruction-filename fallback, or the
7655
+ // bare regex only rewrites that filename and leaves the prefix stale
7656
+ // in place, producing a malformed doubled-prefix path (see the longer
7657
+ // note in convertClaudeToTraeMarkdown above — the instruction filename
7658
+ // and either malformed shape are deliberately never spelled out
7659
+ // contiguously here either, for the same reason: this file ships
7660
+ // verbatim). Both forms target the same concrete file (never a bare
7661
+ // directory), matching the `.md` converter (convertClaudeToTraeMarkdown)
7662
+ // so js/cjs and md content agree on one canonical path.
7663
+ content = content.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
7664
+ content = content.replace(/CLAUDE\.md/g, '.trae/rules/rules.md');
7672
7665
  content = content.replace(/\bClaude Code\b/g, 'Trae');
7673
7666
  return content;
7674
7667
  },
@@ -7786,6 +7779,42 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7786
7779
  // Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
7787
7780
  // Skip generic replacement for Copilot/Antigravity — their converters handle all paths
7788
7781
  let content = fs.readFileSync(srcPath, 'utf8');
7782
+
7783
+ // #2930 (epic #1671 Phase 3): strip `<!-- gsd:section -->` markers
7784
+ // BEFORE any per-runtime rewrite so a `.claude/` -> `.windsurf/` regex
7785
+ // (or any other converter below) never reaches inside a marker
7786
+ // attribute and corrupts it. composeWorkflow is a no-op (byte-identical
7787
+ // return) for the 88+ workflows and every non-workflow .md that carries
7788
+ // no markers, and for a malformed marker it throws loudly naming
7789
+ // srcPath — never emit a half-composed workflow.
7790
+ //
7791
+ // Scoped to gsd-core/workflows/ ONLY (two independent reviewers,
7792
+ // chore/2930): copyWithPathReplacement is the emit path for every .md
7793
+ // under gsd-core/, skills/, and commands/ (see the three call sites),
7794
+ // not just workflows. A doc that merely DOCUMENTS the marker syntax
7795
+ // with an unfenced example (docs/reference/workflow-fragments.md is
7796
+ // the live instance of this class, though not under the install tree
7797
+ // today) would otherwise get silently mis-parsed as a real marker and
7798
+ // that line lossily dropped — a file class issue #2930 never scoped
7799
+ // to. Path is normalized UNCONDITIONALLY (backslash paths arrive on
7800
+ // Linux too — CONTEXT.md path-separator rule) and checked as a
7801
+ // path-segment match so the recursive descent (srcPath may be several
7802
+ // directory levels below gsd-core/workflows/) is still caught.
7803
+ //
7804
+ // #3072: the scoping predicate itself now lives in ONE place —
7805
+ // shouldCompose (src/mcp-catalog.cts, imported above) — rather than
7806
+ // being re-declared inline here. The MCP served catalog calls the
7807
+ // SAME function to decide what it composes vs serves verbatim, so this
7808
+ // install path and the catalog can never independently drift on what
7809
+ // gets composed (ADR-1671:309, DEFECT.GENERATIVE-FIX; the parity gate
7810
+ // is tests/mcp-catalog-parity.test.cjs). shouldCompose normalizes with
7811
+ // the identical unconditional `.replace(/\\/g, '/')` internally, so
7812
+ // this call is behavior-preserving byte-for-behavior with the regex it
7813
+ // replaces.
7814
+ if (shouldCompose(srcPath)) {
7815
+ content = composeWorkflow(content, { sourcePath: srcPath });
7816
+ }
7817
+
7789
7818
  if (!dispatch.mdSkipGenericRewrite) {
7790
7819
  const globalClaudeRegex = /~\/\.claude\//g;
7791
7820
  const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
@@ -8168,11 +8197,12 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8168
8197
 
8169
8198
  // 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
8170
8199
  // native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
8171
- // resolves ~/.kimi, a sibling of targetDir's ~/.config/agents), so its
8200
+ // resolves ~/.kimi for kimi and ~/.kimi-code for kimi-code (#2755), a sibling
8201
+ // of targetDir's ~/.config/agents), so its
8172
8202
  // cleanup can't be driven by anything under targetDir the way every other
8173
8203
  // hook surface above is.
8174
8204
  if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
8175
- const kimiHooksRoot = resolveKimiHooksTomlDir();
8205
+ const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
8176
8206
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
8177
8207
  const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
8178
8208
  if (kimiHooksCleanup.changed) {
@@ -8219,23 +8249,24 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8219
8249
  }
8220
8250
  }
8221
8251
 
8252
+ // #2544: the marker now lives inside kimi's hooks/ dir — remove it
8253
+ // before the emptiness check below, or the dir would never prune.
8254
+ if (removeCommonJsMarker(kimiHooksDir)) {
8255
+ removedCount++;
8256
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
8257
+ }
8258
+
8222
8259
  try {
8223
8260
  if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
8224
8261
  } catch (_) { /* not empty — leave it */ }
8225
8262
  }
8226
8263
 
8227
- const kimiPkgJsonPath = path.join(kimiHooksRoot, 'package.json');
8228
- if (fs.existsSync(kimiPkgJsonPath)) {
8229
- try {
8230
- const content = fs.readFileSync(kimiPkgJsonPath, 'utf8').trim();
8231
- if (content === '{"type":"commonjs"}') {
8232
- fs.unlinkSync(kimiPkgJsonPath);
8233
- removedCount++;
8234
- console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`);
8235
- }
8236
- } catch (e) {
8237
- // Ignore read errors
8238
- }
8264
+ // Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
8265
+ // used to write it. Exact content match — a user's own package.json in
8266
+ // kimi's native config home is never touched.
8267
+ if (removeCommonJsMarker(kimiHooksRoot)) {
8268
+ removedCount++;
8269
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
8239
8270
  }
8240
8271
  }
8241
8272
 
@@ -8501,7 +8532,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8501
8532
  }
8502
8533
 
8503
8534
  // 4. Remove GSD hooks
8504
- const hooksDir = path.join(targetDir, 'hooks');
8535
+ // #3023: mirror the install site's descriptor-driven bundle dir name.
8536
+ const hooksDir = path.join(targetDir, resolveSharedHooksDirName(runtime));
8505
8537
  if (fs.existsSync(hooksDir)) {
8506
8538
  let hookCount = 0;
8507
8539
  for (const hook of GSD_UNINSTALL_HOOKS) {
@@ -8543,13 +8575,18 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8543
8575
  }
8544
8576
  }
8545
8577
 
8546
- // #2717: remove the CommonJS marker GSD wrote into hooks/ for runtimes that
8547
- // stage .js hooks via dedicated paths (cursor/windsurf/codex) — but ONLY if
8548
- // it still carries GSD's exact content (a user-authored package.json is
8549
- // never deleted). Safe no-op for runtimes whose marker lives at the config
8550
- // root (the shared-bundle path) or that never received one.
8578
+ // Retire the CommonJS marker staged into hooks/. hooks/ is shared space and
8579
+ // is deliberately never rmdir'd here, so the marker must be removed
8580
+ // explicitly or it would be left behind. Removed ONLY when it still carries
8581
+ // GSD's exact content — a user-authored package.json is never deleted.
8582
+ //
8583
+ // #2717 reaches the runtimes that stage .js hooks via dedicated paths
8584
+ // (cursor/windsurf/codex); #2544 reaches the shared-bundle runtimes, whose
8585
+ // marker this PR moves out of the config root and into hooks/. Both land in
8586
+ // the same directory, so one guarded call covers both.
8551
8587
  try {
8552
8588
  if (hooksSurface.removeCommonJsMarkerIfGsdOwned(hooksDir)) {
8589
+ removedCount++;
8553
8590
  console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
8554
8591
  }
8555
8592
  } catch { /* best-effort */ }
@@ -8564,12 +8601,38 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8564
8601
  if (_np) {
8565
8602
  const pluginsDir = path.join(targetDir, _np.dir);
8566
8603
  const pluginPath = path.join(pluginsDir, _np.file);
8604
+ // Tracks whether GSD actually removed anything from pluginsDir. The rmdir
8605
+ // below is gated on it: pruning a directory GSD never wrote to is the same
8606
+ // "don't touch territory GSD didn't fill" violation this issue is about,
8607
+ // just inverted — a user-created but empty plugin/ or extensions/ dir is
8608
+ // theirs, and an uninstall that never removed anything has no business
8609
+ // deleting it.
8610
+ let removedFromPluginsDir = false;
8567
8611
  if (fs.existsSync(pluginPath)) {
8568
8612
  try {
8569
8613
  fs.unlinkSync(pluginPath);
8570
8614
  removedCount++;
8615
+ removedFromPluginsDir = true;
8571
8616
  console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`);
8572
8617
  } catch (_) { /* best-effort */ }
8618
+ }
8619
+ // #2544: the adapter's CommonJS marker sits beside it. Cleaned up OUTSIDE
8620
+ // the adapter-exists guard above — a partial install (or a hand-deleted
8621
+ // adapter) would otherwise strand GSD's marker forever and keep the dir
8622
+ // from ever pruning. Conditioned on the adapter being GONE, though: if the
8623
+ // unlink above failed, pulling the marker out from under a still-present
8624
+ // CommonJS adapter would leave it unloadable. The exact content match
8625
+ // still leaves any user-authored package.json in place.
8626
+ if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
8627
+ removedCount++;
8628
+ removedFromPluginsDir = true;
8629
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${_np.dir}/`);
8630
+ }
8631
+ // Only prune a dir GSD emptied. Pre-fix this rmdir sat inside the
8632
+ // adapter-exists guard, so it could never fire on a dir GSD had not
8633
+ // written to; hoisting it out to catch the marker-only case must not
8634
+ // silently widen it to "any empty plugin dir".
8635
+ if (removedFromPluginsDir) {
8573
8636
  try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
8574
8637
  }
8575
8638
  }
@@ -8630,19 +8693,14 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8630
8693
  }
8631
8694
 
8632
8695
  // 5. Remove GSD package.json (CommonJS mode marker)
8633
- const pkgJsonPath = path.join(targetDir, 'package.json');
8634
- if (fs.existsSync(pkgJsonPath)) {
8635
- try {
8636
- const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
8637
- // Only remove if it's our minimal CommonJS marker
8638
- if (content === '{"type":"commonjs"}') {
8639
- fs.unlinkSync(pkgJsonPath);
8640
- removedCount++;
8641
- console.log(` ${green}✓${reset} Removed GSD package.json`);
8642
- }
8643
- } catch (e) {
8644
- // Ignore read errors
8645
- }
8696
+ // Since #2544 the marker is staged into hooks/ (and the nativePlugin dir,
8697
+ // handled at 4z above) rather than at targetDir. The targetDir removal is
8698
+ // retained to retire the marker written by pre-#2544 installs — same exact
8699
+ // content match as before, so a user-authored package.json is still never
8700
+ // touched.
8701
+ if (removeCommonJsMarker(targetDir)) {
8702
+ removedCount++;
8703
+ console.log(` ${green}✓${reset} Removed GSD package.json (pre-#2544 config-root marker)`);
8646
8704
  }
8647
8705
 
8648
8706
  // 6. Clean up settings.json (remove GSD hooks and statusline)
@@ -9574,7 +9632,10 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9574
9632
  // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
9575
9633
  // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
9576
9634
  if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
9577
- const hooksDir = path.join(configDir, 'hooks');
9635
+ // #3023: manifest keys must track the bundle wherever the descriptor put it,
9636
+ // or uninstall/saveLocalPatches silently orphan the tree.
9637
+ const sharedHooksDirName = resolveSharedHooksDirName(runtime);
9638
+ const hooksDir = path.join(configDir, sharedHooksDirName);
9578
9639
  if (fs.existsSync(hooksDir)) {
9579
9640
  // Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
9580
9641
  // scripts/build-hooks.js) rather than a prefix/extension regex, so the
@@ -9586,7 +9647,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9586
9647
  for (const hook of INSTALLED_HOOK_FILES) {
9587
9648
  const hookPath = path.join(hooksDir, hook);
9588
9649
  if (fs.existsSync(hookPath)) {
9589
- manifest.files['hooks/' + hook] = fileHash(hookPath);
9650
+ manifest.files[sharedHooksDirName + '/' + hook] = fileHash(hookPath);
9590
9651
  }
9591
9652
  }
9592
9653
  // Track hooks/lib/ helpers so saveLocalPatches() can back up user edits
@@ -9595,7 +9656,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9595
9656
  if (fs.existsSync(hooksLibDir)) {
9596
9657
  for (const file of fs.readdirSync(hooksLibDir)) {
9597
9658
  if (GSD_HOOK_LIB_FILES.includes(file)) {
9598
- manifest.files['hooks/lib/' + file] = fileHash(path.join(hooksLibDir, file));
9659
+ manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
9599
9660
  }
9600
9661
  }
9601
9662
  }
@@ -10655,8 +10716,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10655
10716
  }
10656
10717
  }
10657
10718
 
10658
- // Descriptor-driven commands/ output report (#785 — Cursor 1.6 slash commands).
10659
- // Gated by hostBehaviors.reportCommandsDir, not a hardcoded `isCursor` branch (#2089).
10719
+ // Descriptor-driven commands/ output report (currently CodeBuddy).
10720
+ // Cursor retired this parallel surface in #2644 because its skills are
10721
+ // already slash-menu entries as well as model-invocable context.
10660
10722
  if (_hostBehaviors(runtime).reportCommandsDir) {
10661
10723
  const commandsDir = path.join(targetDir, 'commands');
10662
10724
  if (fs.existsSync(commandsDir)) {
@@ -10921,7 +10983,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10921
10983
  const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
10922
10984
  for (const entry of agentEntries) {
10923
10985
  if (entry.isFile() && entry.name.endsWith('.md')) {
10924
- let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
10986
+ const agentSourcePath = path.join(agentsSrc, entry.name);
10987
+ let content = fs.readFileSync(agentSourcePath, 'utf8');
10988
+ // #2995 (epic #1671 Phase 6.4): strip `<!-- gsd:section -->` markers BEFORE
10989
+ // the path-rewrite regexes below, so a rewrite can never reach inside a
10990
+ // marker attribute. No-op (byte-identical) for an unmarked agent.
10991
+ content = composeWorkflow(content, { sourcePath: agentSourcePath });
10925
10992
  // Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo
10926
10993
  const dirRegex = /~\/\.claude\//g;
10927
10994
  const homeDirRegex = /\$HOME\/\.claude\//g;
@@ -11106,22 +11173,30 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11106
11173
  // a safe no-op when the dir is already present.
11107
11174
  fs.mkdirSync(destRootDir, { recursive: true });
11108
11175
 
11109
- // Write package.json to force CommonJS mode for GSD scripts
11110
- // Prevents "require is not defined" errors when project has "type": "module"
11111
- // Node.js walks up looking for package.json - this stops inheritance from project
11112
- const pkgJsonDest = path.join(destRootDir, 'package.json');
11113
- fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
11114
- console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
11176
+ // #3023: the bundle's directory NAME is descriptor-driven — a host that
11177
+ // reserves `hooks/` (pi) must be able to opt out. Resolved once here so the
11178
+ // stage / lib / marker sites can never disagree about where the bundle is.
11179
+ const sharedHooksDirName = resolveSharedHooksDirName(runtime);
11180
+
11181
+ // #2544: the CommonJS marker is NOT written here (destRootDir is the
11182
+ // runtime's shared config root — user-writable territory on OpenCode and
11183
+ // Kilo, where it is the documented place to declare local-plugin npm
11184
+ // dependencies). It is written into hooks/ below, the directory GSD
11185
+ // creates and fills with its own .js scripts, once that directory exists.
11115
11186
 
11116
11187
  let hooksOk = true;
11188
+ // #2544: true once GSD has actually written into destRootDir/hooks/, which
11189
+ // is what licenses the CommonJS marker below.
11190
+ let stagedHooks = false;
11117
11191
 
11118
11192
  // Copy hooks from dist/ (bundled with dependencies)
11119
11193
  // Template paths for the target runtime (replaces '.claude' with correct config dir)
11120
11194
  const hooksSrc = path.join(src, 'hooks', 'dist');
11121
11195
  if (fs.existsSync(hooksSrc)) {
11122
- const hooksDest = path.join(destRootDir, 'hooks');
11196
+ const hooksDest = path.join(destRootDir, sharedHooksDirName);
11123
11197
  fs.mkdirSync(hooksDest, { recursive: true });
11124
11198
  const hookEntries = fs.readdirSync(hooksSrc);
11199
+ if (hookEntries.some((e) => fs.statSync(path.join(hooksSrc, e)).isFile())) stagedHooks = true;
11125
11200
  const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
11126
11201
  for (const entry of hookEntries) {
11127
11202
  const srcFile = path.join(hooksSrc, entry);
@@ -11184,7 +11259,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11184
11259
  }
11185
11260
  }
11186
11261
  if (verifyInstalled(hooksDest, 'hooks')) {
11187
- console.log(` ${green}✓${reset} Installed hooks (bundled)`);
11262
+ console.log(` ${green}✓${reset} Installed ${sharedHooksDirName} (bundled)`);
11188
11263
  // Warn if expected community .sh hooks are missing (non-fatal)
11189
11264
  const expectedShHooks = ['gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh', 'gsd-graphify-update.sh'];
11190
11265
  for (const sh of expectedShHooks) {
@@ -11213,10 +11288,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11213
11288
  // below; this helper itself only checks source presence.)
11214
11289
  const hooksLibSrc = path.join(src, 'hooks', 'lib');
11215
11290
  if (fs.existsSync(hooksLibSrc)) {
11216
- const hooksLibDest = path.join(destRootDir, 'hooks', 'lib');
11291
+ const hooksLibDest = path.join(destRootDir, sharedHooksDirName, 'lib');
11217
11292
  fs.mkdirSync(hooksLibDest, { recursive: true });
11218
11293
  copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
11219
- console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
11294
+ if (GSD_HOOK_LIB_FILES.some((f) => fs.existsSync(path.join(hooksLibDest, f)))) stagedHooks = true;
11295
+ console.log(` ${green}✓${reset} Installed ${sharedHooksDirName}/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
11296
+ }
11297
+
11298
+ // #2544: pin the staged hook scripts to CommonJS from inside hooks/ — the
11299
+ // directory GSD just created and filled — instead of from destRootDir.
11300
+ // Scoping the marker to GSD's own directory keeps `require` working in
11301
+ // hooks/*.js and hooks/lib/*.js under any ambient "type": "module", while
11302
+ // leaving the shared config root untouched.
11303
+ //
11304
+ // Gated on `stagedHooks`, NOT on the directory merely existing: hooks/ is
11305
+ // shared space, so an existence check would drop a GSD marker into a
11306
+ // hooks/ directory the user created and GSD never wrote to — the same
11307
+ // write-into-someone-else's-territory this issue is about. And never
11308
+ // written over a package.json GSD does not own.
11309
+ //
11310
+ // ALSO gated on `hooksOk`: `stagedHooks` is computed from the SOURCE
11311
+ // listing before the copy loop, so it stays true when the copies land but
11312
+ // `verifyInstalled` then fails. Marking a hooks/ GSD did not successfully
11313
+ // populate as CommonJS claims an ownership the install did not earn — the
11314
+ // two flags answer different questions ("did we intend to fill it" vs "is
11315
+ // it actually filled"), and the marker needs both.
11316
+ const hooksMarkerDir = path.join(destRootDir, sharedHooksDirName);
11317
+ if (stagedHooks && hooksOk) {
11318
+ switch (ensureCommonJsMarker(hooksMarkerDir)) {
11319
+ case 'written':
11320
+ console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
11321
+ break;
11322
+ case 'preserved-foreign':
11323
+ console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker) — GSD hooks may not resolve as CommonJS`);
11324
+ break;
11325
+ case 'failed':
11326
+ // Best-effort: a read-only or full config dir must not abort the
11327
+ // install with a raw stack trace. The hooks themselves are staged.
11328
+ console.warn(` ${yellow}⚠${reset} Could not write ${sharedHooksDirName}/package.json (CommonJS mode) — install continued; GSD hooks may not resolve as CommonJS`);
11329
+ break;
11330
+ default:
11331
+ break;
11332
+ }
11220
11333
  }
11221
11334
 
11222
11335
  return hooksOk;
@@ -11667,6 +11780,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11667
11780
  const codexHooksDest = path.join(targetDir, 'hooks');
11668
11781
  fs.mkdirSync(codexHooksDest, { recursive: true });
11669
11782
  const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
11783
+ // #2544: track whether anything was actually staged. hooks/dist existing
11784
+ // is not the same as an allowlisted file landing in it — see the marker
11785
+ // gate below.
11786
+ let codexStagedHooks = false;
11670
11787
  for (const entry of fs.readdirSync(codexHooksSrc)) {
11671
11788
  if (!CODEX_HOOKS_TO_COPY.includes(entry)) continue;
11672
11789
  const srcFile = path.join(codexHooksSrc, entry);
@@ -11700,6 +11817,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11700
11817
  fs.copyFileSync(srcFile, destFile);
11701
11818
  try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows */ }
11702
11819
  }
11820
+ codexStagedHooks = true;
11703
11821
  }
11704
11822
  console.log(` ${green}✓${reset} Installed hooks (Codex)`);
11705
11823
  // #2717: write the CommonJS marker into hooks/ alongside the staged .js
@@ -11710,7 +11828,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11710
11828
  // gsd-context-monitor.js as ESM and their require() calls fail silently.
11711
11829
  // Reuses the same helper the Cursor/Windsurf writers call so the marker
11712
11830
  // content + user-file-preservation contract is identical everywhere.
11713
- if (hooksSurface.ensureCommonJsMarker(codexHooksDest)) {
11831
+ //
11832
+ // #2544: gated on codexStagedHooks, mirroring installSharedHooksBundle's
11833
+ // `stagedHooks`. The enclosing guard only proves hooks/dist EXISTS; if it
11834
+ // holds none of CODEX_HOOKS_TO_COPY, this block mkdirs hooks/ and stages
11835
+ // nothing, and an ungated marker would claim a directory GSD did not fill.
11836
+ if (codexStagedHooks && hooksSurface.ensureCommonJsMarker(codexHooksDest)) {
11714
11837
  console.log(` ${green}✓${reset} Wrote hooks/package.json (CommonJS mode)`);
11715
11838
  }
11716
11839
  }
@@ -11931,7 +12054,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11931
12054
  // hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
11932
12055
  // kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
11933
12056
  // independent hooksSurface: kimi's native config.toml [[hooks]] array, which
11934
- // lives outside targetDir entirely (resolveKimiHooksTomlDir resolves ~/.kimi,
12057
+ // lives outside targetDir entirely (resolveKimiHooksTomlDir resolves the
12058
+ // per-runtime root — ~/.kimi for kimi, ~/.kimi-code for kimi-code, #2755 —
11935
12059
  // a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
11936
12060
  // this early-return, rather than requiring installSurface to change.
11937
12061
  //
@@ -11952,7 +12076,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11952
12076
  // ~/.kimi/hooks/<script> rather than a script that doesn't exist under
11953
12077
  // targetDir/hooks (which kimi no longer receives).
11954
12078
  if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
11955
- const kimiHooksRoot = resolveKimiHooksTomlDir();
12079
+ const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
11956
12080
  // Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
11957
12081
  // process.exit(1)`) runs earlier in this function, before this
11958
12082
  // profile-marker-only branch is ever reached — pushing to it here would
@@ -11961,6 +12085,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11961
12085
  if (!installSharedHooksBundle(kimiHooksRoot)) {
11962
12086
  console.warn(` ${yellow}⚠${reset} Kimi hook bundle did not verify at ${path.join(kimiHooksRoot, 'hooks')} — GSD lifecycle hooks may be incomplete`);
11963
12087
  }
12088
+ // #2544: retire the pre-fix marker at kimi's root. installSharedHooksBundle
12089
+ // used to write {"type":"commonjs"} at destRootDir itself; it now writes it
12090
+ // under destRootDir/hooks/, so on an upgrade the old root file is stale and
12091
+ // would keep ~/.kimi pinned to CommonJS.
12092
+ //
12093
+ // Done HERE rather than in installer-migration 007 (which retires the same
12094
+ // stale marker for every other runtime) because kimi's hook root is
12095
+ // the per-runtime Kimi root — resolved by resolveKimiHooksTomlDir, OUTSIDE the configDir.
12096
+ // Migration relPaths are structurally confined to configDir, so the
12097
+ // framework cannot address this path at all. Same exact-content predicate
12098
+ // either way, so a user-authored ~/.kimi/package.json is never touched.
12099
+ if (removeCommonJsMarker(kimiHooksRoot)) {
12100
+ console.log(` ${green}✓${reset} Removed stale package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
12101
+ }
11964
12102
  const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
11965
12103
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
11966
12104
  const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
@@ -13073,14 +13211,26 @@ function maybeSuggestPathExport(globalBin, homeDir) {
13073
13211
 
13074
13212
  console.log('');
13075
13213
  console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
13076
- console.log(` Add it with one of:`);
13077
13214
  const projected = projectPersistentPathExportActions({
13078
13215
  targetDir: globalBin,
13079
13216
  platform: process.platform,
13080
13217
  });
13081
- for (const action of projected.shellActions) {
13082
- const labelPrefix = action.label ? `${action.label}: ` : '';
13083
- console.log(` ${cyan}${labelPrefix}${action.command}${reset}`);
13218
+ if (projected.reason === PATH_ACTION_REASON.WIN32_RESERVED_QUOTE) {
13219
+ // #3118 review MINOR: a win32 targetDir containing `"` makes
13220
+ // projectPathActionProjection return [] (no command can quote it safely
13221
+ // on Windows) — printing the "Add it with one of:" header with nothing
13222
+ // under it is a silent dead-end. Name the cause instead.
13223
+ console.log(` No command can be suggested: the path contains a ${cyan}"${reset} character, which cannot appear in a Windows path.`);
13224
+ } else if (projected.shellActions.length === 0) {
13225
+ // #3118: no target directory to talk about (reason === NO_TARGET_DIR, or
13226
+ // no reason at all) — there is nothing to print beyond the "not on your
13227
+ // PATH" line above.
13228
+ } else {
13229
+ console.log(` Add it with one of:`);
13230
+ for (const action of projected.shellActions) {
13231
+ const labelPrefix = action.label ? `${action.label}: ` : '';
13232
+ console.log(` ${cyan}${labelPrefix}${action.command}${reset}`);
13233
+ }
13084
13234
  }
13085
13235
  console.log('');
13086
13236
  }
@@ -13295,7 +13445,6 @@ module.exports = {
13295
13445
  applyRuntimeContentRewritesInPlace,
13296
13446
  getCodexSkillAdapterHeader,
13297
13447
  convertClaudeCommandToCursorSkill,
13298
- convertClaudeCommandToCursorCommand,
13299
13448
  convertClaudeAgentToCursorAgent,
13300
13449
  convertClaudeAgentToCodexAgent,
13301
13450
  generateCodexAgentToml,
@@ -13330,6 +13479,9 @@ module.exports = {
13330
13479
  // #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
13331
13480
  _resolveHostBehaviors,
13332
13481
  FALLBACK_HOST_BEHAVIORS,
13482
+ // #3023 — shared hook bundle directory name, descriptor-driven
13483
+ SHARED_HOOKS_DIR_DEFAULT,
13484
+ resolveSharedHooksDirName,
13333
13485
  convertSlashCommandsToCodexSkillMentions,
13334
13486
  convertClaudeCommandToCodexSkill,
13335
13487
  convertClaudeCommandToKimiSkill,
@@ -13511,7 +13663,19 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
13511
13663
  console.error('Usage: node install.js --skills-root <runtime>');
13512
13664
  process.exit(1);
13513
13665
  }
13514
- const skillsRoot = getGlobalSkillsBase(runtimeArg);
13666
+ // #3024: validate the runtime id against the shipped capability registry
13667
+ // BEFORE resolving anything. getGlobalSkillsBase's bare `runtimes[runtime]`
13668
+ // lookup falls through the prototype chain to claude's skills root for an
13669
+ // unregistered/hostile id (`__proto__`, `constructor`, `prototype`, …)
13670
+ // instead of failing loudly. isRegisteredRuntimeId is the SAME validator
13671
+ // gsd-tools' `routeSkillsRoot` calls, so this entry point and the shipped
13672
+ // `gsd-tools query skills-root` entry point can never diverge on which
13673
+ // runtime ids they accept.
13674
+ if (!isRegisteredRuntimeId(runtimeArg)) {
13675
+ console.error(`Unknown runtime "${runtimeArg}" — must be a registered runtime id`);
13676
+ process.exit(1);
13677
+ }
13678
+ const skillsRoot = getGlobalSkillsBase(runtimeArg.trim());
13515
13679
  if (skillsRoot === null) {
13516
13680
  console.error(`${runtimeArg} does not use a skills directory`);
13517
13681
  process.exit(1);