@opengsd/gsd-core 1.11.0 → 1.12.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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
package/bin/install.js CHANGED
@@ -41,6 +41,7 @@ const {
41
41
  // consentRequired, hostPrecedenceRank) instead of the id being re-derived
42
42
  // and re-interpreted at each call site. See src/install-scope.cts.
43
43
  const { resolveScope } = require('../gsd-core/bin/lib/install-scope.cjs');
44
+ const { isTestHomeGuardRefusal } = require('../gsd-core/bin/lib/real-home-guard.cjs');
44
45
  // getDirName (runtime -> local config dir name) is relocated out of this
45
46
  // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
46
47
  // conversion module's rewrite engine can consume it without importing
@@ -410,7 +411,7 @@ const GSD_CHANGESET_FILES = [
410
411
  'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
411
412
  'README.md', // documentation only — not user-authored
412
413
  ];
413
- const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs'];
414
+ const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs'];
414
415
 
415
416
  /**
416
417
  * Resolve a runtime's shared-hooks directory name from its descriptor.
@@ -459,19 +460,32 @@ function resolveSharedHooksDirName(runtime) {
459
460
  return name;
460
461
  }
461
462
 
462
- const CODEX_AGENT_SANDBOX = {
463
- 'gsd-executor': 'workspace-write',
464
- 'gsd-planner': 'workspace-write',
465
- 'gsd-phase-researcher': 'workspace-write',
466
- 'gsd-project-researcher': 'workspace-write',
467
- 'gsd-research-synthesizer': 'workspace-write',
468
- 'gsd-verifier': 'workspace-write',
469
- 'gsd-codebase-mapper': 'workspace-write',
470
- 'gsd-roadmapper': 'workspace-write',
471
- 'gsd-debugger': 'workspace-write',
472
- 'gsd-plan-checker': 'read-only',
473
- 'gsd-integration-checker': 'read-only',
474
- };
463
+ // #3897 rung 3 — sandbox_mode derivation, the hold list, and the hold-roster
464
+ // validator now live in `src/codex-agent-toml.cts` (compiled to
465
+ // `gsd-core/bin/lib/codex-agent-toml.cjs`), NOT here. This module used to be
466
+ // the sole owner, and `agent-install-check.cts`'s `checkCodexSandboxPosture`
467
+ // lazily `require()`d THIS FILE to reach `deriveCodexSandboxMode` — but
468
+ // requiring `bin/install.js` runs its whole top-level script, including the
469
+ // CLI's ASCII banner print to stdout, which corrupted every stdout-JSON
470
+ // caller downstream of that posture check (`gsd-tools validate agents`).
471
+ // `codex-agent-toml.cjs` is a genuine leaf (no top-level side effects), so
472
+ // both this file and `agent-install-check.cts` import the derivation from
473
+ // there — ONE owner, no second predicate. See that module's header for the
474
+ // full rationale, and CAUSE B (below, `installCodexConfig`) for the removal
475
+ // of `validateCodexSandboxHolds`'s call from the install runtime path.
476
+ const {
477
+ CODEX_SANDBOX_HOLDS,
478
+ deriveCodexSandboxMode,
479
+ validateCodexSandboxHolds,
480
+ // #3897 list-form parse fix, Fix 3 (generative-fix-divergence): this
481
+ // file's own `generateCodexAgentToml` used to pull `tools:` via its
482
+ // private `extractFrontmatterField` (single-line only) instead of this
483
+ // shared reader — the two sandbox-feeding paths (this emitter and
484
+ // `agent-install-check.cts`'s `checkCodexSandboxPosture`) silently
485
+ // disagreed on YAML block-list `tools:` form. Both now route through this
486
+ // ONE extractor.
487
+ extractToolsValue,
488
+ } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'codex-agent-toml.cjs'));
475
489
 
476
490
  // Copilot tool name mapping — Claude Code tools to GitHub Copilot tools
477
491
  // Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision)
@@ -668,6 +682,68 @@ function _resolveScopeSafe(id, runtime) {
668
682
  }
669
683
  }
670
684
 
685
+ /**
686
+ * Resolve a layout kind's on-disk destination directory. Shared by the
687
+ * skills-root resolution above and the #3664 warning below so the
688
+ * home-override + destSubpath join exists once (#3659-class re-encoding guard).
689
+ */
690
+ function _kindDestDir(layout, kindName, targetDir) {
691
+ const kind = layout.kinds.find((k) => k.kind === kindName);
692
+ if (!kind) return null;
693
+ return path.join(kind.home || targetDir, kind.destSubpath);
694
+ }
695
+
696
+ /**
697
+ * #3738: scope-aware, layout-resolving wrapper over _kindDestDir for callers
698
+ * that have (runtime, configDir, scope) rather than a resolved Layout — the
699
+ * writeManifest agents surface being the first. Never throws: a runtime whose
700
+ * layout cannot be resolved (unknown id, descriptor error) keeps the caller's
701
+ * own fallback rather than losing the manifest.
702
+ */
703
+ function _kindDestDirSafe(runtime, configDir, scope, kindName) {
704
+ try {
705
+ return _kindDestDir(resolveRuntimeArtifactLayout(runtime, configDir, scope), kindName, configDir);
706
+ } catch {
707
+ return null;
708
+ }
709
+ }
710
+
711
+ /**
712
+ * #3664 — warn (never refuse) when `--config-dir` points the install at a
713
+ * directory whose agent destination already holds FOREIGN (non-GSD) agent
714
+ * files — the fingerprint of another harness's config home (e.g. ~/.junie,
715
+ * ~/.factory) or a hand-curated agents dir. GSD emits the selected runtime's
716
+ * artifacts verbatim: tool IDs (`Skill`) and MCP grants (`mcp__server__tool`)
717
+ * that are inert or invalid in a foreign harness surface only at dispatch
718
+ * time, months later. Warn-and-proceed is the issue-sanctioned option (b):
719
+ * a fresh custom dir (the documented brand-specific-dir use), a gsd-only dir
720
+ * (updates, the --all shared dir — including kimi's root `gsd.md`, which is
721
+ * GSD-owned despite the bare `gsd` stem), and the no-flag default-home path
722
+ * (users keep personal agents in ~/.claude/agents) all stay silent. Degrades
723
+ * silently on any resolution failure — the warn path never blocks install.
724
+ */
725
+ function warnIfForeignAgentDest(runtime, targetDir, scope, explicitConfigDir) {
726
+ if (explicitConfigDir !== true) return;
727
+ try {
728
+ const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
729
+ // kimi's global agents kind is `kimi-agents` (#2095 EoS), every other
730
+ // runtime's is `agents`.
731
+ const agentsDir = _kindDestDir(layout, 'agents', targetDir)
732
+ || _kindDestDir(layout, 'kimi-agents', targetDir);
733
+ if (!agentsDir) return;
734
+ if (!fs.existsSync(agentsDir) || !fs.statSync(agentsDir).isDirectory()) return;
735
+ const foreign = fs
736
+ .readdirSync(agentsDir)
737
+ .filter((f) => f.endsWith('.md') && !f.startsWith('gsd-') && f !== 'gsd.md');
738
+ if (foreign.length === 0) return;
739
+ console.log(
740
+ ` ${yellow}⚠${reset} ${bold}${targetDir}${reset} already contains ${foreign.length} non-GSD agent file(s) — this may be another harness's config home. GSD emits artifacts shaped for ${runtime}: tool IDs and MCP grants may be inert or invalid for whatever harness reads this directory (#3664).`
741
+ );
742
+ } catch (_) {
743
+ /* never block install on the warning path */
744
+ }
745
+ }
746
+
671
747
  /**
672
748
  * Resolve the ACTUAL on-disk skills-install directory for a runtime, honoring a
673
749
  * skills-kind `home` override (ADR-1239 upgrade 3 / #2088: e.g. Codex skills ->
@@ -678,8 +754,8 @@ function _resolveScopeSafe(id, runtime) {
678
754
  function _resolveSkillsRootDir(runtime, targetDir, scope) {
679
755
  try {
680
756
  const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
681
- const skillsKind = layout.kinds.find((k) => k.kind === 'skills');
682
- if (skillsKind) return path.join(skillsKind.home || targetDir, skillsKind.destSubpath);
757
+ const skillsDir = _kindDestDir(layout, 'skills', targetDir);
758
+ if (skillsDir) return skillsDir;
683
759
  } catch (_e) { /* fall through to the configDir default */ }
684
760
  return path.join(targetDir, 'skills');
685
761
  }
@@ -700,6 +776,7 @@ function _runtimeAdapter(runtime) {
700
776
  }
701
777
  }
702
778
  const {
779
+ acquireInstallMigrationLock,
703
780
  applyInstallerMigrationPlan,
704
781
  discoverInstallerMigrations,
705
782
  MANIFEST_SCHEMA_VERSION,
@@ -812,6 +889,17 @@ const hasSkillsRoot = args.includes('--skills-root');
812
889
  const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
813
890
  const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
814
891
  const hasDryRun = args.includes('--dry-run');
892
+ // #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
893
+ // orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
894
+ // block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
895
+ // the same bytes for the same root, since the command paths derive from the
896
+ // hooks root and not from the runtime — so no inspection can tell "litter GSD
897
+ // wrote for kimi-code" from "Kimi CLI's working hooks". Cleaning unasked would
898
+ // break #2755's own acceptance criterion ("Uninstalling GSD hooks for one
899
+ // runtime does not touch or remove the other runtime's hooks") for anyone with
900
+ // both products installed. The user, who knows which products they run, is the
901
+ // only party that can decide — so they ask for it explicitly.
902
+ const hasReclaimKimiLegacy = args.includes('--reclaim-kimi-legacy');
815
903
  // --profile=<name> or --profile=<n1>,<n2> (composable); mutually exclusive with --minimal
816
904
  const _profileArgRaw = (() => {
817
905
  for (const arg of args) {
@@ -911,6 +999,21 @@ function disambiguateKimiVariant(runtimes) {
911
999
  return notices;
912
1000
  }
913
1001
 
1002
+ // #3031: `--reclaim-kimi-legacy` only ever acts inside the kimi-code GLOBAL
1003
+ // install branch. Say so when it cannot act, rather than exiting 0 having
1004
+ // silently done nothing: the user asked for a cleanup, and silence is
1005
+ // indistinguishable from "it ran and found nothing". Not a hard error — it
1006
+ // stays composable with `--all`, where it is legitimately inert for the other
1007
+ // seventeen runtimes.
1008
+ if (hasReclaimKimiLegacy && !selectedRuntimes.includes('kimi-code')) {
1009
+ console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — it applies only to a --kimi-code install; nothing in ~/.kimi was touched.`);
1010
+ } else if (hasReclaimKimiLegacy && hasLocal) {
1011
+ // Scope, checked HERE rather than inside install(): kimi-code declares
1012
+ // hostBehaviors.localInstallDeferred, so install() returns early long before
1013
+ // the kimi-hooks-toml branch — a warning placed there would be unreachable.
1014
+ console.error(`${yellow}⚠ --reclaim-kimi-legacy ignored${reset} — the legacy root is a global location; re-run with --global to reclaim it.`);
1015
+ }
1016
+
914
1017
  if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) {
915
1018
  const kimiNotices = disambiguateKimiVariant(selectedRuntimes);
916
1019
  for (const notice of kimiNotices) {
@@ -1075,6 +1178,14 @@ function parseConfigDirFromArgs(argsArray) {
1075
1178
  return null;
1076
1179
  }
1077
1180
 
1181
+ // Parse --no-legacy-cleanup (#3799) — skip the legacy get-shit-done-cc scan
1182
+ // entirely. Some users run gsd-core alongside a live legacy install on
1183
+ // purpose; the scan is best-effbelt cleanup, never load-bearing for the
1184
+ // install itself.
1185
+ function parseNoLegacyCleanupArg(args = process.argv) {
1186
+ return args.includes('--no-legacy-cleanup');
1187
+ }
1188
+
1078
1189
  // Parse --config-dir argument
1079
1190
  function parseConfigDirArg() {
1080
1191
  const result = parseConfigDirFromArgs(args);
@@ -1105,7 +1216,7 @@ if (hasUninstall) {
1105
1216
 
1106
1217
  // Show help if requested
1107
1218
  if (hasHelp) {
1108
- 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`);
1219
+ 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}--kimi-code${reset} Install for Kimi Code 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}--no-legacy-cleanup${reset} Skip the legacy get-shit-done-cc artifact scan\n (an explicit --config-dir already scopes the scan to it)\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 and resolve the node runner at hook-fire time via\n hooks/gsd-node-runner.sh (WSL/Docker bind-mount\n setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--reclaim-kimi-legacy${reset} With --kimi-code: also remove the GSD hooks a\n pre-1.10.0 --kimi-code install orphaned in ~/.kimi.\n Opt-in — those artifacts are indistinguishable from\n Kimi CLI's own, so skip it if you use Kimi CLI too.\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 Code globally (its own ~/.kimi-code root)${reset}\n npx ${pkg.name} --kimi-code --global\n\n ${dim}# Kimi Code, also reclaiming hooks a pre-1.10.0 install left in ~/.kimi${reset}\n npx ${pkg.name} --kimi-code --global --reclaim-kimi-legacy\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 Kimi CLI and Kimi Code are separate products with separate hook roots: use ${cyan}--kimi${reset} (${cyan}~/.kimi${reset}, ${cyan}KIMI_SHARE_DIR${reset}) or ${cyan}--kimi-code${reset} (${cyan}~/.kimi-code${reset}, ${cyan}KIMI_CODE_HOME${reset}).\n`);
1109
1220
  process.exit(0);
1110
1221
  }
1111
1222
 
@@ -1125,6 +1236,10 @@ if (hasHelp) {
1125
1236
  // had no internal caller and no export consumer for it — hooksSurface owns
1126
1237
  // the single implementation now, used internally by resolveNodeRunner there.)
1127
1238
  const resolveNodeRunner = hooksSurface.resolveNodeRunner;
1239
+ // #3662: the runtime-resolving runner token for managed JS hooks — the baked
1240
+ // absolute node path tried FIRST, then `command -v node`, then well-known
1241
+ // layouts, resolved by the shell at hook-fire time instead of bake time.
1242
+ const buildNodeRunnerChainToken = hooksSurface.buildNodeRunnerChainToken;
1128
1243
  const resolveBashRunner = hooksSurface.resolveBashRunner;
1129
1244
  // referencesHook: pure predicate over hook entry objects, shared between
1130
1245
  // install() and finishInstall() (ADR-857 phase 5f-1b).
@@ -1377,10 +1492,18 @@ function readSettings(settingsPath) {
1377
1492
  }
1378
1493
 
1379
1494
  /**
1380
- * Write settings.json with proper formatting
1495
+ * Write settings.json with proper formatting.
1496
+ *
1497
+ * Atomic (temp+rename) because hosts discard the ENTIRE settings file on any
1498
+ * parse failure, so a truncated write costs the user every hook, permission,
1499
+ * and statusline they have — not just GSD's entries. This is the sole writer
1500
+ * of that surface for six runtimes.
1501
+ *
1502
+ * `atomicWriteFileSync` is declared further down this file; it is dereferenced
1503
+ * at call time, after module evaluation, so the ordering is safe.
1381
1504
  */
1382
1505
  function writeSettings(settingsPath, settings) {
1383
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
1506
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1384
1507
  }
1385
1508
 
1386
1509
  // #2875 Part 2 (J8): model-override resolution (readGsdGlobalModelOverrides /
@@ -2253,7 +2376,8 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2253
2376
  /**
2254
2377
  * Apply Antigravity-specific content conversion — path replacement + command name conversion.
2255
2378
  * Path mappings depend on install mode:
2256
- * Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
2379
+ * Global: ~/.claude/skills/ → ~/.gemini/config/skills/ (#3738),
2380
+ * ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/
2257
2381
  * Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/
2258
2382
  * Applied to ALL Antigravity content (skills, agents, engine files).
2259
2383
  * @param {string} content - Source content to convert
@@ -2262,6 +2386,19 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2262
2386
  function convertClaudeToAntigravityContent(content, isGlobal = false) {
2263
2387
  let c = content;
2264
2388
  if (isGlobal) {
2389
+ // #3738: global skills install under ~/.gemini/config/skills (the dir AGY
2390
+ // scans for global discovery), so skills-path references must divert there
2391
+ // — BEFORE the configHome rewrite below, which is correct for gsd-core
2392
+ // runtime-file references (settings, workflows, VERSION) but wrong for the
2393
+ // skills dir itself. Mirrors src/runtime-artifact-conversion.cts (ADR-1508
2394
+ // keeps bin/install.js hand-authored; the two copies must stay in sync).
2395
+ c = c.replace(/\$HOME\/\.claude\/skills\//g, '$HOME/.gemini/config/skills/');
2396
+ c = c.replace(/~\/\.claude\/skills\//g, '~/.gemini/config/skills/');
2397
+ // Bare skills form (no trailing slash) — must also precede the generic
2398
+ // slash rule, which would otherwise divert it to the retired configHome
2399
+ // path ($HOME/.gemini/antigravity/skills).
2400
+ c = c.replace(/\$HOME\/\.claude\/skills\b/g, '$HOME/.gemini/config/skills');
2401
+ c = c.replace(/~\/\.claude\/skills\b/g, '~/.gemini/config/skills');
2265
2402
  c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/');
2266
2403
  c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/');
2267
2404
  // Bare form (no trailing slash) — must come after slash form to avoid double-replace
@@ -3903,10 +4040,39 @@ function _resetCodexWarningDedupeForTests() {
3903
4040
  * @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig
3904
4041
  */
3905
4042
  function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null, sandboxTier = 'codex-agent-sandbox') {
3906
- const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
3907
4043
  const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
3908
4044
  const frontmatterText = frontmatter || '';
4045
+ // #3897 list-form parse fix, Fix 3: `toolsRaw` MUST come from the same
4046
+ // shared `extractToolsValue` reader `checkCodexSandboxPosture` uses, not
4047
+ // this file's own `extractFrontmatterField` — the two used to disagree on
4048
+ // YAML block-list `tools:` form (`extractFrontmatterField`'s single-line
4049
+ // regex read only the first list item), which is exactly the generative-
4050
+ // fix-divergence shape CLAUDE.md warns about for two paths feeding one
4051
+ // derivation. `extractToolsValue` does its own `---`-delimited frontmatter
4052
+ // scan of the full `agentContent`, so it is not re-derived from
4053
+ // `frontmatterText` here.
4054
+ const toolsRaw = extractToolsValue(agentContent) ?? '';
3909
4055
  const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName;
4056
+ // #3897 rung 3 — derived from the role's own tool contract (HALT.md option
4057
+ // 2). The former hand-maintained CODEX_AGENT_SANDBOX map is deleted (ADR-3473
4058
+ // §8.3): it was fully redundant with this derivation, zero disagreements
4059
+ // across all 11 entries. Never a silent `|| 'read-only'` fallback either.
4060
+ // Derivation itself lives in codex-agent-toml.cjs, which does no frontmatter
4061
+ // parsing of its own (no third copy of that extraction) — it takes the
4062
+ // already-resolved `tools:` value, extracted via the shared reader above.
4063
+ //
4064
+ // #3897 security review F1 (blocker): pass BOTH candidate identities —
4065
+ // `agentName` (the caller's filename-stem identity) AND `resolvedName`
4066
+ // (the frontmatter `name:` this function's OWN emitted `name = ...` line
4067
+ // uses, and what `installCodexConfig`'s caller keys the output PATH on) —
4068
+ // never just one. Deciding the sandbox for `agentName` alone and applying
4069
+ // it to an artifact that a DIFFERENT identity (`resolvedName`) names is
4070
+ // exactly how a held role's own `.toml` could end up `workspace-write`
4071
+ // (rename the source file, or plant a sibling whose `name:` collides with
4072
+ // a held role). `deriveCodexSandboxMode` takes the most restrictive result
4073
+ // across every candidate — see its doc and `isSandboxHeld` in
4074
+ // codex-agent-toml.cjs.
4075
+ const sandboxMode = deriveCodexSandboxMode([agentName, resolvedName], toolsRaw);
3910
4076
  const resolvedDescription = toSingleLine(
3911
4077
  extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}`
3912
4078
  );
@@ -3986,8 +4152,10 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3986
4152
  // #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
3987
4153
  // as the Claude .md effort injection (resolveInstallTimeEffort), so both runtimes read
3988
4154
  // from the same effort.agent_overrides / effort.routing_tier_defaults / effort.default
3989
- // config source. Codex does not support 'max' → clamped to 'xhigh' by
3990
- // gsdRenderEffortForRuntime('codex', ...).
4155
+ // config source. #3007 — Codex advertises supported_reasoning_levels per model, so the
4156
+ // pinned model id is passed through and the value is resolved against that model's own
4157
+ // set: 'max' now passes, 'minimal' clamps up to 'low', and 'ultra' is refused (no key
4158
+ // emitted) rather than clamped to a fabricated level.
3991
4159
  // #838 — Do not pin effort when Codex is intentionally inheriting the parent
3992
4160
  // chat model. A TOML with no `model` but a static `model_reasoning_effort`
3993
4161
  // creates confusing partial routing: model follows the Codex UI while effort
@@ -3997,8 +4165,12 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3997
4165
  // #3533 (10d): 'inherit' means OMIT the pin — the agent follows the host's
3998
4166
  // own effort default. Never write the literal.
3999
4167
  if (_universalEffortCodex !== 'inherit') {
4000
- const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
4001
- lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
4168
+ const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex, pinnedModel).value;
4169
+ // #3007 — 'ultra' is rejected by the model's supported_reasoning_levels and
4170
+ // renders as null. Omit the key entirely rather than write a literal `null`.
4171
+ if (_renderedEffortCodex !== null) {
4172
+ lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
4173
+ }
4002
4174
  }
4003
4175
  }
4004
4176
 
@@ -6206,6 +6378,31 @@ const __atomicWrittenTmps = hooksSurface.__atomicWrittenTmps;
6206
6378
  * All writes go through atomicWriteFileSync so a mid-write failure leaves
6207
6379
  * the original config.toml untouched (#2760 fix 4).
6208
6380
  */
6381
+ /**
6382
+ * Split TOML content into its leading TOP-LEVEL key lines and everything from
6383
+ * the first table header onward (#3610).
6384
+ *
6385
+ * Top-level keys were file-scoped before a merge. The regenerated GSD block
6386
+ * opens with a table header (`[agents]`, #2088/ADR-1239 upgrade 2), so placing
6387
+ * that block ABOVE surviving top-level keys re-scopes them into `[agents]` —
6388
+ * `validateCodexConfigSchema` then correctly rejects the merged file and the
6389
+ * install aborts. Hoisting the keys above the block preserves their scope.
6390
+ *
6391
+ * Table headers inside multiline strings do not start the "rest" region (the
6392
+ * record parser already excludes them via startsInMultilineString).
6393
+ */
6394
+ function splitTopLevelKeys(content) {
6395
+ for (const record of getTomlLineRecords(content)) {
6396
+ if (record.tableHeader && !record.startsInMultilineString) {
6397
+ return {
6398
+ topLevel: content.slice(0, record.start).trim(),
6399
+ rest: content.slice(record.start).trim(),
6400
+ };
6401
+ }
6402
+ }
6403
+ return { topLevel: content.trim(), rest: '' };
6404
+ }
6405
+
6209
6406
  function mergeCodexConfig(configPath, gsdBlock) {
6210
6407
  // Case 1: No config.toml — create fresh
6211
6408
  if (!fs.existsSync(configPath)) {
@@ -6253,10 +6450,25 @@ function mergeCodexConfig(configPath, gsdBlock) {
6253
6450
  .replace(/^\r?\n# GSD codex_hooks ownership: (?:section|root_dotted)\r?\n/, '');
6254
6451
  const afterUser = stripLeakedGsdCodexSections(markerStripped).trim();
6255
6452
 
6453
+ // #3610: top-level keys that survived BELOW the marker were file-scoped
6454
+ // before this merge; the regenerated block opens with the `[agents]` table
6455
+ // header, so they must be hoisted to FILE scope or TOML re-scopes them
6456
+ // into a table. File scope means BEFORE the first table header of the
6457
+ // pre-marker region too — appending them after a pre-marker table (the
6458
+ // default real-world layout: user tables precede the marker) would merely
6459
+ // capture them into THAT table instead of [agents], and the schema
6460
+ // validator is blind to non-agents tables.
6461
+ const beforeSplit = before ? splitTopLevelKeys(before) : { topLevel: '', rest: '' };
6462
+ const { topLevel: afterTopLevel, rest: afterTables } = splitTopLevelKeys(afterUser);
6463
+
6256
6464
  const parts = [];
6257
- if (before) parts.push(before);
6465
+ const topParts = [];
6466
+ if (beforeSplit.topLevel) topParts.push(beforeSplit.topLevel);
6467
+ if (afterTopLevel) topParts.push(afterTopLevel);
6468
+ if (topParts.length > 0) parts.push(topParts.join(eol + eol));
6469
+ if (beforeSplit.rest) parts.push(beforeSplit.rest);
6258
6470
  parts.push(normalizedGsdBlock);
6259
- if (afterUser) parts.push(afterUser);
6471
+ if (afterTables) parts.push(afterTables);
6260
6472
  atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
6261
6473
  return;
6262
6474
  }
@@ -6726,29 +6938,50 @@ function writeNonClaudeDefaults(runtime) {
6726
6938
  if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
6727
6939
  const gsdDir = path.join(os.homedir(), '.gsd');
6728
6940
  const defaultsPath = path.join(gsdDir, 'defaults.json');
6941
+ let releaseLock = null;
6729
6942
  try {
6730
6943
  fs.mkdirSync(gsdDir, { recursive: true });
6944
+ // defaults.json is machine-global — every runtime and project on the box
6945
+ // reads it. Serialize the read-modify-write so two concurrent installs
6946
+ // cannot lose each other's key, and apply both mutations in ONE atomic
6947
+ // write so a crash cannot leave the file truncated (the read path swallows
6948
+ // parse errors and treats a corrupt file as absent, which would silently
6949
+ // degrade model resolution everywhere until repaired by hand).
6950
+ releaseLock = acquireInstallMigrationLock(gsdDir);
6731
6951
  let defaults = {};
6732
6952
  try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
6733
6953
  if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
6734
6954
  defaults = {};
6735
6955
  }
6956
+ const applied = [];
6736
6957
  // Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
6737
6958
  const existing = defaults.resolve_model_ids;
6738
6959
  const shouldDefaultToOmit = existing !== true && existing !== 'omit';
6739
6960
  if (shouldDefaultToOmit) {
6740
6961
  defaults.resolve_model_ids = 'omit';
6741
- fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
6742
- console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
6962
+ applied.push(`Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
6743
6963
  }
6744
6964
  // #2395: persist runtime for non-Claude runtimes.
6745
6965
  if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
6746
6966
  defaults.runtime = runtime;
6747
- fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
6748
- console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
6967
+ applied.push(`Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
6968
+ }
6969
+ if (applied.length > 0) {
6970
+ atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
6971
+ for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
6749
6972
  }
6750
6973
  } catch (e) {
6751
6974
  console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
6975
+ } finally {
6976
+ if (releaseLock) {
6977
+ try {
6978
+ releaseLock();
6979
+ } catch (releaseError) {
6980
+ // A leaked lock blocks the next install, so surface it rather than
6981
+ // swallowing; the stale-lock reaper clears it once this pid exits.
6982
+ console.log(` ${yellow}⚠${reset} Could not release the ~/.gsd install lock: ${releaseError.message}`);
6983
+ }
6984
+ }
6752
6985
  }
6753
6986
  }
6754
6987
 
@@ -6772,6 +7005,36 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
6772
7005
  }
6773
7006
  fs.mkdirSync(agentsTomlDir, { recursive: true });
6774
7007
 
7008
+ // #3897 rung 3 (CAUSE B fix) — validateCodexSandboxHolds is deliberately
7009
+ // NOT called here. The "no stale holds" invariant is a REPO invariant about
7010
+ // the canonical roster in `agents/`, not a property of whatever directory
7011
+ // an install happens to read from: a partial or synthetic `agentsSrc` (a
7012
+ // test fixture, a `--config-dir` subset) legitimately contains only a few
7013
+ // agents, and a held role simply absent from THIS source dir must be
7014
+ // inert, not fatal. Throwing here also masked unrelated failures further
7015
+ // down this same loop (e.g. the name-injection/path-escape guard on
7016
+ // `agentTomlPath` below), since this check ran first and unconditionally.
7017
+ // The invariant is still enforced — as a test over the real `agents/`
7018
+ // roster (tests/codex-config.test.cjs T24/T25) — just never on this
7019
+ // runtime path. See src/codex-agent-toml.cts's `validateCodexSandboxHolds`
7020
+ // docblock for the full rationale.
7021
+ //
7022
+ // #3897 security review F2 (assessed post-F1-fix, verified by execution —
7023
+ // not asserted): before F1's fix, the "runtime detector removed" gap here
7024
+ // was real — a rename (case a) or a sibling `name:` clobber (case b) could
7025
+ // reach this loop and land a held role's `.toml` at `workspace-write` with
7026
+ // nothing here to catch it. After F1 (`deriveCodexSandboxMode` now decides
7027
+ // over BOTH the filename stem and the resolved frontmatter `name:`, most
7028
+ // restrictive wins), both cases were re-run end-to-end through this exact
7029
+ // function and the EMITTED ARTIFACT for both is `read-only` — the
7030
+ // dangerous condition no longer produces a wrong artifact, it produces the
7031
+ // SAFE one. A detector guarding a now-fail-safe condition is not
7032
+ // load-bearing, and restoring a throw here would re-break the legitimate
7033
+ // partial-source-dir case CAUSE B removed it for (see above). Regression
7034
+ // coverage for both cases lives in `tests/codex-config.test.cjs` (F1(a)
7035
+ // rename / F1(b) sibling-clobber rows), asserted on the emitted `.toml`'s
7036
+ // `sandbox_mode`, not on the derivation's return value.
7037
+
6775
7038
  const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
6776
7039
  const agents = [];
6777
7040
 
@@ -6800,7 +7063,20 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
6800
7063
  // CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
6801
7064
  content = convertClaudeToCodexMarkdown(content);
6802
7065
  const { frontmatter } = extractFrontmatterAndBody(content);
6803
- const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '');
7066
+ // #3897 security review F1 (blocker, post-merge): this loop used to key
7067
+ // the sandbox/hold decision off ONLY the filename stem while the emitted
7068
+ // `.toml`'s OUTPUT PATH below is keyed off `name` (frontmatter-derived,
7069
+ // attacker-editable) — so a renamed source file, or a sibling file whose
7070
+ // `name:` collides with a held role, could make the decided identity and
7071
+ // the landed artifact disagree, widening a held role's own file to
7072
+ // `workspace-write`. `generateCodexAgentToml` (below) now derives
7073
+ // `sandbox_mode` over BOTH the filename stem it is given AND the
7074
+ // frontmatter `name:` it resolves internally, taking the most
7075
+ // restrictive result — this loop no longer needs to choose one identity
7076
+ // for that call; see `deriveCodexSandboxMode`'s doc in
7077
+ // `codex-agent-toml.cts` for the resolution.
7078
+ const fileStem = file.replace(/\.md$/, '');
7079
+ const name = extractFrontmatterField(frontmatter, 'name') || fileStem;
6804
7080
  const description = extractFrontmatterField(frontmatter, 'description') || '';
6805
7081
 
6806
7082
  agents.push({ name, description: toSingleLine(description) });
@@ -6822,7 +7098,10 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
6822
7098
  // #443 — pass unified effort config so model_reasoning_effort in the .toml
6823
7099
  // follows the same config-driven precedence as the Claude .md effort key.
6824
7100
  const effortCfg = readGsdEffectiveEffortConfig(targetDir);
6825
- const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
7101
+ // Pass `fileStem`; `generateCodexAgentToml` itself additionally resolves
7102
+ // and folds in the frontmatter `name:` for the sandbox decision (F1
7103
+ // above) — this call site does not need to pass `name` explicitly.
7104
+ const tomlContent = generateCodexAgentToml(fileStem, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
6826
7105
  // Confine the per-agent write to the agents/ dir itself: a crafted agent
6827
7106
  // `name` containing path separators must not escape agents/ (which would let
6828
7107
  // it clobber config.toml or write elsewhere under the configHome).
@@ -7822,6 +8101,133 @@ function validateHookFields(settings) {
7822
8101
  */
7823
8102
  const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd'];
7824
8103
 
8104
+ /**
8105
+ * Whether two paths denote the SAME directory — used to stop a reclaim from
8106
+ * deleting the very root the current install just wrote (#3031).
8107
+ *
8108
+ * A plain `path.resolve` comparison is not enough here, because both roots come
8109
+ * from user-controlled env vars (`KIMI_SHARE_DIR`, `KIMI_CODE_HOME`) and two
8110
+ * different strings routinely name one directory:
8111
+ * - case-insensitive filesystems (macOS, Windows): `~/Kimi` vs `~/kimi`
8112
+ * - symlinks / bind mounts: `~/link-to-kimi` vs the real target
8113
+ * Getting this wrong is not cosmetic — it is the difference between skipping a
8114
+ * reclaim and deleting a live install's own hooks.
8115
+ *
8116
+ * Strategy, cheapest-first: string equality after `resolve`, then identity by
8117
+ * `dev`+`ino` (definitive when both exist and the platform reports them), then
8118
+ * `realpath` string equality (resolves symlinks AND canonicalizes case). Any
8119
+ * rung answering "same" wins; a path that does not exist cannot be the root we
8120
+ * just wrote, so a failed stat simply falls through.
8121
+ *
8122
+ * @returns {boolean} true only when both paths are proven to be one directory.
8123
+ */
8124
+ function isSameDirectory(a, b) {
8125
+ if (path.resolve(a) === path.resolve(b)) return true;
8126
+ try {
8127
+ const sa = fs.statSync(a);
8128
+ const sb = fs.statSync(b);
8129
+ // `ino` is 0 on some Windows filesystems; only trust a positive match.
8130
+ if (sa.ino && sb.ino && sa.dev === sb.dev && sa.ino === sb.ino) return true;
8131
+ } catch (_) { /* one side missing — fall through to realpath */ }
8132
+ try {
8133
+ return fs.realpathSync.native(a) === fs.realpathSync.native(b);
8134
+ } catch (_) {
8135
+ return false;
8136
+ }
8137
+ }
8138
+
8139
+ /**
8140
+ * Remove every GSD-owned artifact from a Kimi hooks root (`~/.kimi` for kimi,
8141
+ * `~/.kimi-code` for kimi-code — resolveKimiHooksTomlDir, #2755): the managed
8142
+ * `[[hooks]]` block in the native config.toml, the hook scripts, hooks/lib/,
8143
+ * and the CommonJS marker at both its current (hooks/) and pre-#2544 (root)
8144
+ * locations.
8145
+ *
8146
+ * This root is Kimi's own native config home — SHARED space that may hold the
8147
+ * user's real config.toml, providers and their own scripts — so only exact
8148
+ * GSD-owned filenames are removed and directories are pruned only when that
8149
+ * removal leaves them empty.
8150
+ *
8151
+ * TWO callers, deliberately one implementation (#3031). `uninstall()` calls it
8152
+ * for the runtime being uninstalled; the opt-in `--reclaim-kimi-legacy` path in
8153
+ * `install()` calls it for the LEGACY `~/.kimi` root a pre-#2755 `--kimi-code`
8154
+ * install orphaned. Duplicating this sequence for the second caller would be
8155
+ * exactly the generative-divergence hazard the repo bans — the reclaim must
8156
+ * remove precisely what a real uninstall removes, forever, by construction.
8157
+ *
8158
+ * @param {string} kimiHooksRoot - Absolute path to the Kimi hooks root.
8159
+ * @returns {number} count of removal steps performed (0 when nothing matched).
8160
+ */
8161
+ function reclaimKimiHooksRoot(kimiHooksRoot) {
8162
+ let steps = 0;
8163
+ const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
8164
+ const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
8165
+ if (kimiHooksCleanup.changed) {
8166
+ steps++;
8167
+ console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
8168
+ }
8169
+
8170
+ // Kimi's shared hook scripts + CommonJS package.json marker are installed
8171
+ // into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
8172
+ // kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
8173
+ // Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
8174
+ // kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
8175
+ // may hold the user's real config.toml/providers), so only the exact
8176
+ // GSD-owned filenames are removed, and directories are pruned only if left
8177
+ // empty by that removal.
8178
+ const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
8179
+ if (fs.existsSync(kimiHooksDir)) {
8180
+ let kimiHookCount = 0;
8181
+ for (const hook of GSD_UNINSTALL_HOOKS) {
8182
+ const hookPath = path.join(kimiHooksDir, hook);
8183
+ if (fs.existsSync(hookPath)) {
8184
+ fs.unlinkSync(hookPath);
8185
+ kimiHookCount++;
8186
+ }
8187
+ }
8188
+ if (kimiHookCount > 0) {
8189
+ steps++;
8190
+ console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
8191
+ }
8192
+
8193
+ const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
8194
+ if (fs.existsSync(kimiHooksLibDir)) {
8195
+ let removedKimiLibFiles = 0;
8196
+ for (const file of GSD_HOOK_LIB_FILES) {
8197
+ try {
8198
+ fs.unlinkSync(path.join(kimiHooksLibDir, file));
8199
+ removedKimiLibFiles++;
8200
+ } catch (_) { /* best-effort */ }
8201
+ }
8202
+ try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
8203
+ if (removedKimiLibFiles > 0) {
8204
+ steps++;
8205
+ console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
8206
+ }
8207
+ }
8208
+
8209
+ // #2544: the marker now lives inside kimi's hooks/ dir — remove it
8210
+ // before the emptiness check below, or the dir would never prune.
8211
+ if (removeCommonJsMarker(kimiHooksDir)) {
8212
+ steps++;
8213
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
8214
+ }
8215
+
8216
+ try {
8217
+ if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
8218
+ } catch (_) { /* not empty — leave it */ }
8219
+ }
8220
+
8221
+ // Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
8222
+ // used to write it. Exact content match — a user's own package.json in
8223
+ // kimi's native config home is never touched.
8224
+ if (removeCommonJsMarker(kimiHooksRoot)) {
8225
+ steps++;
8226
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
8227
+ }
8228
+ return steps;
8229
+ }
8230
+
7825
8231
  /**
7826
8232
  * Uninstall GSD from the specified directory for a specific runtime
7827
8233
  * Removes only GSD-specific files/directories, preserves user content
@@ -8019,72 +8425,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8019
8425
  // cleanup can't be driven by anything under targetDir the way every other
8020
8426
  // hook surface above is.
8021
8427
  if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
8022
- const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
8023
- const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
8024
- const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
8025
- if (kimiHooksCleanup.changed) {
8026
- removedCount++;
8027
- console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
8028
- }
8029
-
8030
- // Kimi's shared hook scripts + CommonJS package.json marker are installed
8031
- // into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
8032
- // kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
8033
- // Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
8034
- // kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
8035
- // may hold the user's real config.toml/providers), so only the exact
8036
- // GSD-owned filenames are removed, and directories are pruned only if left
8037
- // empty by that removal.
8038
- const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
8039
- if (fs.existsSync(kimiHooksDir)) {
8040
- let kimiHookCount = 0;
8041
- for (const hook of GSD_UNINSTALL_HOOKS) {
8042
- const hookPath = path.join(kimiHooksDir, hook);
8043
- if (fs.existsSync(hookPath)) {
8044
- fs.unlinkSync(hookPath);
8045
- kimiHookCount++;
8046
- }
8047
- }
8048
- if (kimiHookCount > 0) {
8049
- removedCount++;
8050
- console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
8051
- }
8052
-
8053
- const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
8054
- if (fs.existsSync(kimiHooksLibDir)) {
8055
- let removedKimiLibFiles = 0;
8056
- for (const file of GSD_HOOK_LIB_FILES) {
8057
- try {
8058
- fs.unlinkSync(path.join(kimiHooksLibDir, file));
8059
- removedKimiLibFiles++;
8060
- } catch (_) { /* best-effort */ }
8061
- }
8062
- try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
8063
- if (removedKimiLibFiles > 0) {
8064
- removedCount++;
8065
- console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
8066
- }
8067
- }
8068
-
8069
- // #2544: the marker now lives inside kimi's hooks/ dir — remove it
8070
- // before the emptiness check below, or the dir would never prune.
8071
- if (removeCommonJsMarker(kimiHooksDir)) {
8072
- removedCount++;
8073
- console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksDir}`);
8074
- }
8075
-
8076
- try {
8077
- if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
8078
- } catch (_) { /* not empty — leave it */ }
8079
- }
8080
-
8081
- // Retire the pre-#2544 marker at kimi's root (~/.kimi), where the bundle
8082
- // used to write it. Exact content match — a user's own package.json in
8083
- // kimi's native config home is never touched.
8084
- if (removeCommonJsMarker(kimiHooksRoot)) {
8085
- removedCount++;
8086
- console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot} (pre-#2544 marker)`);
8087
- }
8428
+ removedCount += reclaimKimiHooksRoot(resolveKimiHooksTomlDir({ runtime }));
8088
8429
  }
8089
8430
 
8090
8431
  // 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
@@ -9450,7 +9791,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9450
9791
  const resolvedScope = options.scope === 'local' ? 'local' : 'global';
9451
9792
  const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
9452
9793
  const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
9453
- const agentsDir = path.join(configDir, 'agents');
9794
+ // #3738: resolve the ACTUAL agents-install dir honoring an agents-kind `home`
9795
+ // override (antigravity global → $HOME/.gemini/config/agents), mirroring
9796
+ // _resolveSkillsRootDir for skills. Hardcoding configDir/agents left the
9797
+ // manifest blind to the whole agents surface the moment the override landed —
9798
+ // no drift detection, no patch backup. Falls back to <configDir>/agents.
9799
+ const agentsDir = _kindDestDirSafe(runtime, configDir, resolvedScope, 'agents')
9800
+ || _kindDestDirSafe(runtime, configDir, resolvedScope, 'kimi-agents')
9801
+ || path.join(configDir, 'agents');
9454
9802
  const manifest = {
9455
9803
  // Schema version of this DOCUMENT (#2872) — distinct from `version`
9456
9804
  // below, which is the GSD package version. Absent ⇒ a pre-#2872 (v1)
@@ -10080,6 +10428,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10080
10428
  ? process.cwd()
10081
10429
  : path.join(process.cwd(), dirName);
10082
10430
 
10431
+ // #3664: a --config-dir destination holding foreign agent files gets an
10432
+ // explicit install-time warning — never a silent Claude-shaped emit.
10433
+ if (isGlobal) {
10434
+ warnIfForeignAgentDest(runtime, targetDir, _installScopeId, Boolean(explicitConfigDir));
10435
+ }
10436
+
10083
10437
  // #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
10084
10438
  // artifact orphaned by a PRIOR install run that died between staging and
10085
10439
  // its own restore/discard, BEFORE this run's own preserve step stages
@@ -11374,10 +11728,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11374
11728
  // abort a successful install — log a warning and continue.
11375
11729
  // install() is never reached in --dry-run mode (the early-exit at the CLI
11376
11730
  // dispatch handles preview), so cleanup here always applies for real.
11377
- try {
11378
- cleanupLegacyGsdCc({ dryRun: false });
11379
- } catch (cleanupErr) {
11380
- console.warn(` ${yellow}Warning: legacy cleanup failed: ${cleanupErr.message}${reset}`);
11731
+ //
11732
+ // #3799: when --config-dir redirected the install, the scan is SCOPED to
11733
+ // that destination ([targetDir]) — the default home's live install must
11734
+ // never be planned for removal from a sandboxed install. --no-legacy-cleanup
11735
+ // skips the scan entirely.
11736
+ const skipNoLegacyCleanup = parseNoLegacyCleanupArg();
11737
+ const legacyCleanupScope = (explicitConfigDir !== null && isGlobal)
11738
+ ? [targetDir]
11739
+ : undefined;
11740
+ if (!skipNoLegacyCleanup) {
11741
+ try {
11742
+ cleanupLegacyGsdCc({ dryRun: false, ...(legacyCleanupScope ? { configDirs: legacyCleanupScope } : {}) });
11743
+ } catch (cleanupErr) {
11744
+ console.warn(` ${yellow}Warning: legacy cleanup failed: ${cleanupErr.message}${reset}`);
11745
+ }
11381
11746
  }
11382
11747
 
11383
11748
  if (failures.length > 0) {
@@ -11480,7 +11845,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11480
11845
  // #3245 CR finding 2 — any throw in the pre-config install operations (skills copy,
11481
11846
  // agents copy, VERSION write, manifest write, etc.) triggers the Codex pre-config
11482
11847
  // rollback so the caller is never left in a partially-installed state.
11483
- rollbackInstallerMigrations();
11848
+ // (The second, identical rollbackInstallerMigrations() that used to sit here was
11849
+ // a duplicate of the line above, not a second phase — removed in #3725 review.)
11850
+ // #3712 — the test-home guard refuses before any LAYOUT-DRIVEN write, so no
11851
+ // gsd-* directory in the skills root has been touched and there is nothing
11852
+ // there to undo. (Legacy install migrations DO run first; that is why the
11853
+ // rollbackInstallerMigrations() calls above still execute, and why the one
11854
+ // migration that can reach a `home` override carries its own assertion.)
11855
+ // Running the codex rollback anyway would delete and recreate every
11856
+ // snapshotted gsd-* directory in the resolved skills root, which for an
11857
+ // un-sandboxed codex install IS the real ~/.agents/skills: the guard's own
11858
+ // refusal would provoke the mutation it exists to prevent. This is the only
11859
+ // _codexPreConfigRollback() call site, and applySurface/uninstall cannot
11860
+ // reach it. Every other error still rolls back. Found by review, not by CI.
11861
+ if (isTestHomeGuardRefusal(_earlyInstallErr)) throw _earlyInstallErr;
11484
11862
  if (_codexPreConfigRollback) {
11485
11863
  _codexPreConfigRollback();
11486
11864
  }
@@ -12024,6 +12402,44 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12024
12402
  if (kimiHooksResult.changed) {
12025
12403
  console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
12026
12404
  }
12405
+
12406
+ // #3031: opt-in reclaim of the pre-#2755 legacy root. Runs LAST in this
12407
+ // branch so the kimi-code install above is already complete and durable —
12408
+ // a reclaim can only ever remove, never leave the install half-written.
12409
+ //
12410
+ // Gated on `runtime === 'kimi-code'`: a `--kimi` install resolves this
12411
+ // very same `~/.kimi` as its own hooks root, so reclaiming there would
12412
+ // delete the hooks it just wrote. The flag is silently inert for kimi
12413
+ // rather than an error — `--all` passes every runtime through this branch,
12414
+ // and one opt-in flag must not fail an otherwise valid multi-runtime run.
12415
+ //
12416
+ // ALSO gated on kimi NOT being installed by this same invocation. The
12417
+ // flag asserts "I only use Kimi Code"; `--all`, or an explicit `--kimi
12418
+ // --kimi-code`, falsifies that outright. Both orderings put `kimi` BEFORE
12419
+ // `kimi-code` (selectRuntimesFromArgs), so without this guard the run
12420
+ // installs Kimi CLI's hooks and then deletes them moments later — the run
12421
+ // reports success and the user is left with the very breakage the opt-in
12422
+ // exists to prevent. Verified reproducible before this guard existed.
12423
+ const kimiInstalledThisRun = selectedRuntimes.includes('kimi');
12424
+ if (hasReclaimKimiLegacy && runtime === 'kimi-code' && kimiInstalledThisRun) {
12425
+ console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: this run also installs --kimi, so ${resolveKimiHooksTomlDir({ runtime: 'kimi' })} is a live Kimi CLI install`);
12426
+ } else if (hasReclaimKimiLegacy && runtime === 'kimi-code') {
12427
+ const legacyKimiRoot = resolveKimiHooksTomlDir({ runtime: 'kimi' });
12428
+ // Both roots honor their own env override (KIMI_SHARE_DIR /
12429
+ // KIMI_CODE_HOME). A user who points both at ONE directory collapses
12430
+ // "the legacy root" onto "the root this install just wrote", and an
12431
+ // unguarded reclaim would delete its own output. isSameDirectory compares
12432
+ // the DIRECTORIES, not the strings — case-insensitive filesystems and
12433
+ // symlinked aliases both name one dir with two spellings.
12434
+ if (isSameDirectory(legacyKimiRoot, kimiHooksRoot)) {
12435
+ console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: ${legacyKimiRoot} is this install's own hooks root`);
12436
+ } else {
12437
+ const reclaimed = reclaimKimiHooksRoot(legacyKimiRoot);
12438
+ console.log(reclaimed > 0
12439
+ ? ` ${green}✓${reset} Reclaimed ${reclaimed} orphaned GSD artifact group(s) from ${legacyKimiRoot} (pre-#2755)`
12440
+ : ` ${dim}•${reset} No orphaned GSD artifacts found in ${legacyKimiRoot}`);
12441
+ }
12442
+ }
12027
12443
  }
12028
12444
 
12029
12445
  // ADR-1239 / #2100 Stage 2: Windsurf's own independent hooksSurface —
@@ -12117,9 +12533,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12117
12533
  );
12118
12534
  const hasGsdStatusline = sharedRaw.statusLine && sharedRaw.statusLine.command &&
12119
12535
  isManagedHookCommand(sharedRaw.statusLine.command, { surface: 'settings-json' });
12120
- if (hasGsdHooks || hasGsdStatusline) {
12536
+ const needsMigration = hasGsdHooks || hasGsdStatusline;
12537
+ // readSettings returns null ONLY for an unparseable file — its documented
12538
+ // "preserve existing, don't touch" signal. Stand the WHOLE migration down
12539
+ // in that case: skipping just the local merge while still stripping the
12540
+ // shared file below would destroy the GSD entries outright instead of
12541
+ // relocating them. Leaving both files untouched lets the migration retry
12542
+ // once the user repairs the local file.
12543
+ const localRaw = needsMigration ? readSettings(settingsPath) : null;
12544
+ if (needsMigration && localRaw === null) {
12545
+ console.log(' ' + yellow + 'i' + reset + ' Skipping #338 migration — ' + settingsFileName +
12546
+ ' could not be parsed. Your existing settings are preserved.');
12547
+ } else if (needsMigration) {
12121
12548
  // Merge GSD entries into settings.local.json
12122
- const localRaw = readSettings(settingsPath) || {};
12123
12549
  if (hasGsdStatusline && !localRaw.statusLine) {
12124
12550
  localRaw.statusLine = sharedRaw.statusLine;
12125
12551
  }
@@ -12179,16 +12605,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12179
12605
  if (rawSettings === null) {
12180
12606
  console.log(' ' + yellow + 'i' + reset + ' Skipping settings.local.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
12181
12607
  persistActiveProfileMarker();
12182
- return;
12608
+ // Callers index this result by `runtime` (installAllRuntimes' statusline
12609
+ // lookup), so every early exit must return the full shape — a bare return
12610
+ // crashes the install rather than skipping one file.
12611
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
12183
12612
  }
12184
12613
  const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
12185
- // #3002 CR: rewrite legacy `node .../gsd-*.js` command strings carried over
12186
- // from pre-#2979 installs to use the absolute node binary path. Without this,
12187
- // existing managed hook entries stay bare-`node`-prefixed across reinstalls
12188
- // and remain broken under GUI/minimal-PATH runtimes.
12189
- const settingsRunner = resolveNodeRunner();
12614
+ // #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
12615
+ // #2979 installs) AND entries baked with another environment's absolute node
12616
+ // path onto the runtime-resolving runner. Without this, existing managed
12617
+ // hook entries stay bare-`node`-prefixed or foreign-absolute across
12618
+ // reinstalls and remain broken under GUI/minimal-PATH runtimes and shared
12619
+ // config roots — the #3662 mixed state where no environment can run all
12620
+ // hooks.
12621
+ const settingsRunner = buildNodeRunnerChainToken();
12190
12622
  if (settingsRunner && rewriteLegacyManagedNodeHookCommands(settings, settingsRunner, { platform: process.platform, runtime })) {
12191
- console.log(` ${green}✓${reset} Rewrote legacy bare-node managed-hook commands to absolute path (#2979)`);
12623
+ console.log(` ${green}✓${reset} Rewrote legacy managed-hook commands to the runtime-resolving node runner (#2979/#3662)`);
12192
12624
  }
12193
12625
  // Local installs anchor hook paths so they resolve regardless of cwd (#1906).
12194
12626
  // Claude Code sets $CLAUDE_PROJECT_DIR; Antigravity does not — and on
@@ -12199,12 +12631,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12199
12631
  // check inside projectLocalHookPrefix.
12200
12632
  const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
12201
12633
  const hookOpts = { portableHooks: hasPortableHooks, runtime };
12202
- // #2979: local-install hook commands also use the absolute node path so
12203
- // GUI/minimal-PATH runtimes can resolve them. Bare `node` fails when the
12204
- // host launches the runtime with a stripped PATH (Finder/Antigravity/etc).
12205
- const localNodeRunner = resolveNodeRunner();
12634
+ // #2979: local-install hook commands also use a runner GUI/minimal-PATH
12635
+ // runtimes can resolve. Bare `node` fails when the host launches the
12636
+ // runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
12637
+ // the baked absolute path with the runtime-resolving chain (baked path
12638
+ // first, so the minimal-PATH guarantee is unchanged).
12639
+ const localNodeRunner = buildNodeRunnerChainToken();
12206
12640
  const localBashRunner = resolveBashRunner({ platform: process.platform });
12207
- // If we cannot resolve an absolute node path AND this is a local install,
12641
+ // If we cannot resolve a node runner AND this is a local install,
12208
12642
  // skip managed-hook registration. Returning null from buildHookCommand on
12209
12643
  // global installs has the same effect. Better to skip than to emit a bare
12210
12644
  // `node` command that recreates the #2979 failure.
@@ -13209,31 +13643,49 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
13209
13643
  * @param {object} [opts.logger=console] - injectable logger
13210
13644
  * @returns {{ plan: {path:string,reason:string}[], result: object }}
13211
13645
  */
13212
- function cleanupLegacyGsdCc({ homeDir = os.homedir(), dryRun = false, logger = console } = {}) {
13646
+ function cleanupLegacyGsdCc({ homeDir = os.homedir(), configDirs = null, dryRun = false, logger = console } = {}) {
13213
13647
  // Build de-duplicated list of candidate config dirs to scan.
13214
13648
  // Only scan under homeDir — never cwd — to prevent accidental deletion of
13215
13649
  // the user's active-project hooks when the installer is invoked from a
13216
13650
  // project directory that has .claude/hooks or similar subdirs.
13651
+ // #3799: an explicit configDirs override (install() passes [targetDir]
13652
+ // whenever --config-dir redirected the destination) scopes the WHOLE scan
13653
+ // to that dir — the default-home scan must never plan removals of a live
13654
+ // install that lives outside the destination the user chose.
13217
13655
  const seen = new Set();
13218
- const configDirs = [];
13219
- for (const name of _LEGACY_SCAN_SUBDIR_NAMES) {
13220
- const candidate = path.join(homeDir, name);
13221
- if (!seen.has(candidate) && fs.existsSync(candidate)) {
13222
- seen.add(candidate);
13223
- configDirs.push(candidate);
13656
+ const scanDirs = [];
13657
+ if (Array.isArray(configDirs) && configDirs.length > 0) {
13658
+ for (const candidate of configDirs) {
13659
+ if (!seen.has(candidate) && fs.existsSync(candidate)) {
13660
+ seen.add(candidate);
13661
+ scanDirs.push(candidate);
13662
+ }
13663
+ }
13664
+ } else {
13665
+ for (const name of _LEGACY_SCAN_SUBDIR_NAMES) {
13666
+ const candidate = path.join(homeDir, name);
13667
+ if (!seen.has(candidate) && fs.existsSync(candidate)) {
13668
+ seen.add(candidate);
13669
+ scanDirs.push(candidate);
13670
+ }
13224
13671
  }
13225
13672
  }
13226
13673
 
13227
13674
  // planLegacyCleanup scans each configDir and already includes the legacy
13228
13675
  // shared cache (gsd-update-check.json) as a plan entry.
13229
- const plan = planLegacyCleanup(configDirs, { homeDir });
13676
+ const plan = planLegacyCleanup(scanDirs, { homeDir, ...(Array.isArray(configDirs) && configDirs.length > 0 ? { configDirs } : {}) });
13230
13677
 
13231
13678
  // Apply the plan (dryRun honors the flag).
13232
13679
  const result = applyLegacyCleanup(plan, { dryRun, logger });
13233
13680
 
13234
13681
  // Also clear / preview the per-package cache so next session re-evaluates
13235
13682
  // hook versions (replaces the former inline unlinkSync on line ~9104).
13236
- const perPkgCacheFile = path.join(homeDir, '.cache', 'gsd', updateCacheFileName);
13683
+ // #3799: under a configDirs override the cache is read/cleared under the
13684
+ // SCOPE root, never the default home — same invariant as the scan itself.
13685
+ const perPkgCacheRoot = (Array.isArray(configDirs) && configDirs.length > 0)
13686
+ ? configDirs[0]
13687
+ : homeDir;
13688
+ const perPkgCacheFile = path.join(perPkgCacheRoot, '.cache', 'gsd', updateCacheFileName);
13237
13689
  if (dryRun) {
13238
13690
  logger.log('[dry-run] would remove: ' + perPkgCacheFile + ' (per-package-update-cache)');
13239
13691
  } else {
@@ -13352,7 +13804,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
13352
13804
  });
13353
13805
  };
13354
13806
 
13355
- if (primaryStatuslineResult) {
13807
+ // `settings` is null on every early exit (unparseable file, skipped runtime),
13808
+ // and handleStatusline dereferences it — an install that declined to touch a
13809
+ // settings file has no statusline to prompt about, so fall through.
13810
+ if (primaryStatuslineResult && primaryStatuslineResult.settings) {
13356
13811
  handleStatusline(primaryStatuslineResult.settings, isInteractive, continueAfterStatusline);
13357
13812
  } else if (canInstallBanner) {
13358
13813
  // No statusline-capable runtime, but at least one runtime can host the
@@ -13372,6 +13827,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
13372
13827
  module.exports = {
13373
13828
  // #3677 — hyphen-namespace normalization seam for agent bodies
13374
13829
  shouldNormalizeHyphenNamespaceInAgentBody,
13830
+ // #3664: --config-dir foreign-agent-destination warning (warn-and-proceed)
13831
+ warnIfForeignAgentDest,
13375
13832
  normalizeAgentBodyForRuntime,
13376
13833
  yamlIdentifier,
13377
13834
  getCodexSkillAdapterHeader,
@@ -13426,7 +13883,10 @@ module.exports = {
13426
13883
  GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
13427
13884
  GSD_CLAUDE_DENY_PERMISSIONS,
13428
13885
  GSD_CODEX_MARKER,
13429
- CODEX_AGENT_SANDBOX,
13886
+ // #3897 rung 3 (ADR-3473 §8.3, HALT.md option 2)
13887
+ CODEX_SANDBOX_HOLDS,
13888
+ deriveCodexSandboxMode,
13889
+ validateCodexSandboxHolds,
13430
13890
  getGlobalDir,
13431
13891
  getConfigDirFromHome,
13432
13892
  resolveKiloConfigPath,
@@ -13505,6 +13965,8 @@ module.exports = {
13505
13965
  cleanupLegacyGsdCc,
13506
13966
  // #1191 — exported so tests exercise the REAL readSettings, not a replica
13507
13967
  readSettings,
13968
+ writeSettings,
13969
+ writeNonClaudeDefaults,
13508
13970
  stripJsonComments,
13509
13971
  copyWithPathReplacement,
13510
13972
  };
@@ -13519,10 +13981,23 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
13519
13981
  console.log('Dry run — no files will be modified.\n');
13520
13982
  // cleanupLegacyGsdCc with dryRun:true is the single source of truth for
13521
13983
  // both the legacy artifacts and the per-package cache path — no duplicate
13522
- // printing here.
13523
- const { plan } = cleanupLegacyGsdCc({ dryRun: true });
13524
- if (plan.length === 0) {
13525
- console.log(' (no legacy get-shit-done-cc artifacts found)');
13984
+ // printing here. #3799: the preview honors the SAME scope and skip the
13985
+ // real install would apply (--config-dir scopes; --no-legacy-cleanup
13986
+ // skips) — a preview that listed default-home paths a real install would
13987
+ // never touch misrepresents the run.
13988
+ if (parseNoLegacyCleanupArg()) {
13989
+ console.log(' (--no-legacy-cleanup — legacy scan skipped)');
13990
+ } else {
13991
+ const previewScope = (explicitConfigDir !== null)
13992
+ ? [getGlobalConfigDir(DEFAULT_RUNTIME, explicitConfigDir)]
13993
+ : undefined;
13994
+ const { plan } = cleanupLegacyGsdCc({
13995
+ dryRun: true,
13996
+ ...(previewScope ? { configDirs: previewScope } : {}),
13997
+ });
13998
+ if (plan.length === 0) {
13999
+ console.log(' (no legacy get-shit-done-cc artifacts found)');
14000
+ }
13526
14001
  }
13527
14002
  process.exit(0);
13528
14003
  } else if (hasSkillsRoot) {