@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
package/bin/install.js CHANGED
@@ -436,7 +436,18 @@ const GSD_CHANGESET_FILES = [
436
436
  'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
437
437
  'README.md', // documentation only — not user-authored
438
438
  ];
439
- 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', 'shellcheck-fetch.cjs'];
439
+ 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', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs', 'macos-conformance-tier.generated.cjs'];
440
+
441
+ // #4544 — the Codex hook payload the install stages into <targetDir>/hooks/.
442
+ // Hoisted to module scope (the #3184 precedent above) so the rollback's
443
+ // incomplete-capture path can name exactly the files GSD owns without a
444
+ // second copy of the list drifting away from the staging site, which lives
445
+ // inside the Codex config block where the constant used to be declared.
446
+ const CODEX_HOOKS_TO_COPY = [
447
+ 'gsd-check-update.js',
448
+ 'gsd-check-update-worker.js',
449
+ 'managed-hooks-registry.cjs',
450
+ ];
440
451
 
441
452
  /**
442
453
  * Resolve a runtime's shared-hooks directory name from its descriptor.
@@ -544,6 +555,13 @@ const {
544
555
  RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
545
556
  isAnthropicFlavoredModel: gsdIsAnthropicFlavoredModel,
546
557
  } = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
558
+ // #4145: shared hash-first recovery for gsd-pristine/ baselines stored at an
559
+ // unexpected path (e.g. without the gsd-core/ prefix an earlier release's
560
+ // writer dropped). Same module the reapply verifier uses, so the two readers
561
+ // cannot drift apart again.
562
+ const {
563
+ findPristineByHash: gsdFindPristineByHash,
564
+ } = require(path.join(_gsdLibDir, 'pristine-baseline.cjs'));
547
565
  // #2875 Part 2: MODEL_PROFILES + resolveTierEntry are now consumed only by
548
566
  // install-model-override-resolver.cjs's readGsdRuntimeProfileResolver
549
567
  // (required below) — this installer no longer needs its own bindings.
@@ -805,6 +823,7 @@ const {
805
823
  applyInstallerMigrationPlan,
806
824
  discoverInstallerMigrations,
807
825
  MANIFEST_SCHEMA_VERSION,
826
+ readInstallManifest,
808
827
  runInstallerMigrations,
809
828
  } = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
810
829
  const {
@@ -918,6 +937,25 @@ const hasSkillsRoot = args.includes('--skills-root');
918
937
  const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
919
938
  const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
920
939
  const hasDryRun = args.includes('--dry-run');
940
+ // #4377: emit project-relative `@` includes (`.claude/gsd-core/...`) for a
941
+ // LOCAL install instead of this checkout's absolute path.
942
+ //
943
+ // Opt-in, and it stays opt-in: absolute includes work for a single checkout,
944
+ // which is nearly everyone, and flipping the default would change every
945
+ // existing local install to solve a problem those users do not have. The
946
+ // people who need it know they do — they run the same repo from several git
947
+ // worktrees, where a baked absolute path means every worktree reads its
948
+ // workflow prose out of whichever checkout happened to run the installer, and
949
+ // updating that one checkout breaks all the others at once with no way to
950
+ // stage it.
951
+ //
952
+ // Exported through the environment rather than threaded as a parameter,
953
+ // exactly like --portable-hooks/GSD_PORTABLE_HOOKS above: five separate seams
954
+ // compute a path prefix (the install engine, both rewrite entry points, the
955
+ // install plan, and applySurface), and one variable they all read cannot fall
956
+ // out of sync the way five signatures can.
957
+ const hasRelativeIncludes = args.includes('--relative-includes') || process.env.GSD_RELATIVE_INCLUDES === '1';
958
+ if (hasRelativeIncludes) process.env.GSD_RELATIVE_INCLUDES = '1';
921
959
  // #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
922
960
  // orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
923
961
  // block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
@@ -1245,7 +1283,7 @@ if (hasUninstall) {
1245
1283
 
1246
1284
  // Show help if requested
1247
1285
  if (hasHelp) {
1248
- 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`);
1286
+ 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}--relative-includes${reset} With --local: write project-relative @ includes\n (.claude/gsd-core/...) instead of this checkout's\n absolute path, so several git worktrees of one repo\n each read their own copy (also GSD_RELATIVE_INCLUDES=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}# Local install for a repo worked from several git worktrees${reset}\n npx ${pkg.name} --claude --local --relative-includes\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`);
1249
1287
  process.exit(0);
1250
1288
  }
1251
1289
 
@@ -1285,6 +1323,7 @@ const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
1285
1323
  // callers continue to work and there is a single implementation. (All call
1286
1324
  // sites are below this line, so the const binding has no TDZ hazard.)
1287
1325
  const processAttribution = runtimeArtifactConversion.processAttribution;
1326
+ const filterRuntimeNotesForTarget = runtimeArtifactConversion.filterRuntimeNotesForTarget;
1288
1327
  // computePathPrefix: implementation lives in runtimeArtifactConversion
1289
1328
  // (ADR-1508 / #1511 Phase 2 — single owner). Re-bound here so install.js call
1290
1329
  // sites continue to work. #2876 retired the sibling
@@ -1362,34 +1401,13 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
1362
1401
  }
1363
1402
 
1364
1403
  /**
1365
- * Ensure hooks.json contains exactly one managed GSD hook entry for the given
1366
- * Codex event, wired to gsd-context-monitor.js. Preserves user-owned entries.
1367
- *
1368
- * Used for the new Codex events added in #772:
1369
- * SubagentStart — inject context / GSD_AGENT_NAME awareness at subagent open
1370
- * Stop — post-session context headroom tracking
1371
- * PostToolUse — mirror the Claude Code PostToolUse context monitor
1372
- *
1373
- * All three events are routed through gsd-context-monitor.js — the same hook
1374
- * used for PostToolUse in the Claude Code baseline — so context-headroom
1375
- * warnings surface at these key Codex session lifecycle moments.
1376
- *
1377
- * On Windows (#3426): writes a gsd-context-monitor.cmd shim alongside the .js
1378
- * file and uses the .cmd path as the hook command — exactly the same fix as
1379
- * SessionStart uses for gsd-check-update — to avoid the bash.exe POSIX-exec
1380
- * failure when Codex's hook dispatcher tries to run node.exe through Git Bash.
1381
- *
1382
- * @param {string} targetDir
1383
- * @param {string} eventName - One of 'SubagentStart', 'Stop', 'PostToolUse'.
1384
- * @param {{ absoluteRunner: string|null, platform?: NodeJS.Platform }} opts
1385
- * @returns {{ changed: boolean, wrote: boolean, path: string }}
1386
- */
1387
- function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
1388
- return hooksSurface.ensureCodexHooksJsonEvent(targetDir, eventName, opts);
1389
- }
1390
-
1391
- /**
1392
- * Remove a GSD-managed event entry from hooks.json. Called during uninstall.
1404
+ * Remove a GSD-managed event entry from hooks.json. Called during uninstall,
1405
+ * and (#2586) unconditionally during install/reinstall to clean up a
1406
+ * pre-#2586 install's stale gsd-context-monitor.js registrations — GSD no
1407
+ * longer ADDS entries for these events (see CODEX_HOOKS_TO_COPY /
1408
+ * cleanupOrphanedCodexContextMonitorScript in bin/install.js's Codex branch),
1409
+ * only removes recognized ones, so the `ensureCodexHooksJsonEvent` wrapper
1410
+ * that used to add them was removed as dead code.
1393
1411
  *
1394
1412
  * @param {string} targetDir
1395
1413
  * @param {string} eventName
@@ -1638,19 +1656,20 @@ const claudeToOpencodeTools = {
1638
1656
  WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
1639
1657
  };
1640
1658
 
1641
- // Tool name mapping from Claude Code to Gemini CLI
1642
- // Gemini CLI uses snake_case built-in tool names
1643
- const claudeToGeminiTools = {
1644
- Read: 'read_file',
1659
+ // Tool name mapping from Claude Code to Antigravity
1660
+ // Antigravity uses Gemini's snake_case built-in tool names
1661
+ const claudeToAntigravityTools = {
1662
+ // #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
1663
+ Read: 'view_file',
1645
1664
  Write: 'write_file',
1646
- Edit: 'replace',
1647
- Bash: 'run_shell_command',
1665
+ Edit: 'replace_file_content',
1666
+ Bash: 'run_command',
1648
1667
  Glob: 'glob',
1649
- Grep: 'search_file_content',
1668
+ Grep: 'grep_search',
1650
1669
  WebSearch: 'google_web_search',
1651
1670
  WebFetch: 'web_fetch',
1652
1671
  TodoWrite: 'write_todos',
1653
- };
1672
+ }
1654
1673
 
1655
1674
  // Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
1656
1675
  // Kimi custom agent YAML requires fully-qualified module paths.
@@ -1700,24 +1719,24 @@ function convertToolName(claudeTool) {
1700
1719
  }
1701
1720
 
1702
1721
  /**
1703
- * Convert a Claude Code tool name to Gemini CLI format
1704
- * - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.)
1705
- * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini
1706
- * - Filters out Task/Agent — agents are auto-registered as tools in Gemini
1707
- * @returns {string|null} Gemini tool name, or null if tool should be excluded
1722
+ * Convert a Claude Code tool name to Antigravity format
1723
+ * - Applies Claude→Antigravity mapping (Read→read_file, Bash→run_shell_command, etc.)
1724
+ * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Antigravity
1725
+ * - Filters out Task/Agent — agents are auto-registered as tools in Antigravity
1726
+ * @returns {string|null} Antigravity tool name, or null if tool should be excluded
1708
1727
  */
1709
- function convertGeminiToolName(claudeTool) {
1728
+ function convertAntigravityToolName(claudeTool) {
1710
1729
  // MCP tools: exclude — auto-discovered from mcpServers config at runtime
1711
1730
  if (claudeTool.startsWith('mcp__')) {
1712
1731
  return null;
1713
1732
  }
1714
1733
  // Task/Agent: exclude — agents are auto-registered as callable tools.
1715
- // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool;
1716
- // emitting it causes frontmatter validation errors (#3362).
1717
- // Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool;
1718
- // the lowercase fallback would emit an invalid 'skill'/'slashcommand' name
1719
- // that fails frontmatter validation (tools.N: Invalid tool name) and aborts
1720
- // the entire agent load (#1394).
1734
+ // AskUserQuestion: exclude — Antigravity (Gemini tool dialect) does not expose
1735
+ // an ask_user tool; emitting it causes frontmatter validation errors (#3362).
1736
+ // Skill/SlashCommand: exclude — Antigravity (Gemini tool dialect) has no 'skill'
1737
+ // built-in tool; the lowercase fallback would emit an invalid
1738
+ // 'skill'/'slashcommand' name that fails frontmatter validation
1739
+ // (tools.N: Invalid tool name) and aborts the entire agent load (#1394).
1721
1740
  if (
1722
1741
  claudeTool === 'Task' ||
1723
1742
  claudeTool === 'Agent' ||
@@ -1729,8 +1748,8 @@ function convertGeminiToolName(claudeTool) {
1729
1748
  return null;
1730
1749
  }
1731
1750
  // Check for explicit mapping
1732
- if (claudeToGeminiTools[claudeTool]) {
1733
- return claudeToGeminiTools[claudeTool];
1751
+ if (claudeToAntigravityTools[claudeTool]) {
1752
+ return claudeToAntigravityTools[claudeTool];
1734
1753
  }
1735
1754
  // Default: lowercase
1736
1755
  return claudeTool.toLowerCase();
@@ -2036,7 +2055,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
2036
2055
  const names = cmdNames || readGsdCommandNames();
2037
2056
  const normalizedBody = transformContentToHyphen(body, names);
2038
2057
 
2039
- const description = extractFrontmatterField(frontmatter, 'description') || '';
2058
+ // #4324: the description is the text the host's skill picker renders, so it
2059
+ // needs the same hyphen normalisation the body gets — otherwise a `/gsd:<cmd>`
2060
+ // mention in a command description ships the retired colon form to the user.
2061
+ const description = transformContentToHyphen(
2062
+ extractFrontmatterField(frontmatter, 'description') || '', names,
2063
+ );
2040
2064
  const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
2041
2065
  const agent = extractFrontmatterField(frontmatter, 'agent');
2042
2066
  // #769: preserve context: from source command files so it is emitted into
@@ -2429,7 +2453,7 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2429
2453
  * @param {boolean} [isGlobal=false] - Whether this is a global install
2430
2454
  */
2431
2455
  function convertClaudeToAntigravityContent(content, isGlobal = false) {
2432
- let c = content;
2456
+ let c = filterRuntimeNotesForTarget(content, 'antigravity');
2433
2457
  if (isGlobal) {
2434
2458
  // #3738: global skills install under ~/.gemini/config/skills (the dir AGY
2435
2459
  // scans for global discovery), so skills-path references must divert there
@@ -2499,12 +2523,16 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
2499
2523
  const color = extractFrontmatterField(frontmatter, 'color');
2500
2524
  const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2501
2525
 
2502
- // Map tools to Gemini equivalents (reuse existing convertGeminiToolName)
2526
+ // Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName)
2503
2527
  const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2504
- const mappedTools = claudeTools.map(t => convertGeminiToolName(t)).filter(Boolean);
2528
+ const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
2505
2529
 
2506
2530
  // #2876: quote description for the same reason as the skill variant.
2507
- let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`;
2531
+ // #4705: tools is a YAML SEQUENCE of native names (see the src twin).
2532
+ const toolsBlock = mappedTools.length > 0
2533
+ ? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
2534
+ : 'tools: []\n';
2535
+ let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
2508
2536
  if (color) fm += `color: ${color}\n`;
2509
2537
  fm += '---';
2510
2538
 
@@ -2565,7 +2593,7 @@ function convertSlashCommandsToCursorSkillMentions(content) {
2565
2593
  }
2566
2594
 
2567
2595
  function convertClaudeToCursorMarkdown(content) {
2568
- let converted = convertSlashCommandsToCursorSkillMentions(content);
2596
+ let converted = convertSlashCommandsToCursorSkillMentions(filterRuntimeNotesForTarget(content, 'cursor'));
2569
2597
  // Replace tool name references in body text
2570
2598
  converted = converted.replace(/\bBash\(/g, 'Shell(');
2571
2599
  converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
@@ -2686,7 +2714,7 @@ function convertSlashCommandsToTraeSkillMentions(content) {
2686
2714
  }
2687
2715
 
2688
2716
  function convertClaudeToTraeMarkdown(content) {
2689
- let converted = convertSlashCommandsToTraeSkillMentions(content);
2717
+ let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae'));
2690
2718
  converted = converted.replace(/\bBash\(/g, 'Shell(');
2691
2719
  converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
2692
2720
  // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
@@ -2812,7 +2840,7 @@ function convertSlashCommandsToCodebuddySkillMentions(content) {
2812
2840
  }
2813
2841
 
2814
2842
  function convertClaudeToCodebuddyMarkdown(content) {
2815
- let converted = convertSlashCommandsToCodebuddySkillMentions(content);
2843
+ let converted = convertSlashCommandsToCodebuddySkillMentions(filterRuntimeNotesForTarget(content, 'codebuddy'));
2816
2844
  // CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.)
2817
2845
  // No tool name conversion needed
2818
2846
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
@@ -2904,7 +2932,7 @@ function convertClaudeAgentToCodebuddyAgent(content) {
2904
2932
  // ── Cline converters ────────────────────────────────────────────────────────
2905
2933
 
2906
2934
  function convertClaudeToCliineMarkdown(content) {
2907
- let converted = content;
2935
+ let converted = filterRuntimeNotesForTarget(content, 'cline');
2908
2936
  // Cline uses the same tool names as Claude Code — no tool name conversion needed
2909
2937
  converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`');
2910
2938
  converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules');
@@ -2960,6 +2988,8 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
2960
2988
  let description = extractFrontmatterField(frontmatter, 'description');
2961
2989
  if (!description) description = `Run GSD workflow ${skillName}.`;
2962
2990
  description = toSingleLine(description);
2991
+ // #4324: same reason as the Claude skill converter above.
2992
+ description = transformContentToHyphen(description, names);
2963
2993
  // Cline documented max is 1024 code points (not UTF-16 code units).
2964
2994
  // Use Array.from to iterate by code point so that multibyte characters
2965
2995
  // (e.g. emoji, astral-plane chars) are never split, which would produce
@@ -3827,7 +3857,7 @@ function rewriteBareGsdToolsCommandsForCodex(content) {
3827
3857
  }
3828
3858
 
3829
3859
  function convertClaudeToCodexMarkdown(content) {
3830
- let converted = convertSlashCommandsToCodexSkillMentions(content);
3860
+ let converted = convertSlashCommandsToCodexSkillMentions(filterRuntimeNotesForTarget(content, 'codex'));
3831
3861
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
3832
3862
  // Remove /clear references — Codex has no equivalent command
3833
3863
  // Handle backtick-wrapped: `\/clear` then: → (removed)
@@ -3934,6 +3964,22 @@ Typed mapping (agent_type-capable schema only):
3934
3964
  never fabricate a manual worktree protocol — route through the negotiated
3935
3965
  isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
3936
3966
 
3967
+ Foreground handoffs:
3968
+ - spawn_agent is asynchronous. When the source Agent(...) or Task(...) declares
3969
+ run_in_background=false, call collaboration.wait_agent(timeout_ms=...) immediately after
3970
+ spawn and keep the parent turn active until that child returns a terminal result.
3971
+ - collaboration.wait_agent is a mailbox wakeup, NOT a completion oracle: "Wait completed"
3972
+ can mean only that a child sent an interim MESSAGE or status update. After every wakeup,
3973
+ inspect the named child's update/status. Only a FINAL_ANSWER or a terminal agent status
3974
+ (completed, failed, or cancelled) ends the foreground handoff.
3975
+ - On an interim MESSAGE or any non-terminal status, do not report an outcome, send a
3976
+ continuation, start parent work, or end the parent turn. Call collaboration.wait_agent
3977
+ again for the same child. If a terminal response is absent after an abnormal end, reconcile
3978
+ the workflow's durable artifacts before classifying the child.
3979
+ - This applies to one foreground child as well as fan-out. The child retains its workflow's
3980
+ own checkpoint loop; do not report an outcome or start any further parent work before its
3981
+ terminal result is available.
3982
+
3937
3983
  Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
3938
3984
  When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
3939
3985
  (\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
@@ -3961,6 +4007,9 @@ Spawn restriction:
3961
4007
  defaulting to inline execution.
3962
4008
 
3963
4009
  Parallel fan-out:
4010
+ - For each child, loop on collaboration.wait_agent(timeout_ms=...) until its own terminal
4011
+ result is observed. A mailbox update from one child never completes another child, and an
4012
+ interim MESSAGE never completes its sender.
3964
4013
  - Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
3965
4014
  - Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
3966
4015
 
@@ -7208,7 +7257,7 @@ function neutralizeAgentReferences(content, instructionFile) {
7208
7257
 
7209
7258
  function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
7210
7259
  // Replace tool name references in content (applies to all files)
7211
- let convertedContent = content;
7260
+ let convertedContent = filterRuntimeNotesForTarget(content, 'opencode');
7212
7261
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
7213
7262
  convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
7214
7263
  convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
@@ -7370,7 +7419,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7370
7419
  // tests/runtime-converters.test.cjs (#2093).
7371
7420
  function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
7372
7421
  // Replace tool name references in content (applies to all files)
7373
- let convertedContent = content;
7422
+ let convertedContent = filterRuntimeNotesForTarget(content, 'kilo');
7374
7423
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
7375
7424
  convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
7376
7425
  convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
@@ -7924,45 +7973,66 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7924
7973
  content = composeWorkflow(content, { sourcePath: srcPath });
7925
7974
  }
7926
7975
 
7976
+ content = filterRuntimeNotesForTarget(content, runtime);
7977
+
7927
7978
  if (!dispatch.mdSkipGenericRewrite) {
7928
- const globalClaudeRegex = /~\/\.claude\//g;
7929
- const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
7930
- const localClaudeRegex = /\.\/\.claude\//g;
7931
- content = content.replace(globalClaudeRegex, pathPrefix);
7932
- content = content.replace(globalClaudeHomeRegex, pathPrefix);
7933
- content = content.replace(localClaudeRegex, `./${dirName}/`);
7934
- // #3544 review (Finding 1 fallout): guarded with the SAME
7935
- // negative-lookahead convention already used at ~:2859-2860 below
7936
- // ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
7937
- // is satisfied by ANY non-word character, including '-' — so for a
7938
- // --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
7939
- // pathPrefix '$HOME/.claude-work/'), this pass re-matched the
7940
- // '$HOME/.claude' PREFIX of its own slash-form output (lines above)
7941
- // and re-appended the full prefix, corrupting every emitted path to
7942
- // '$HOME/.claude-work-work/...'. Harmless no-op for the literal
7943
- // default '.claude' (self-replace with an identical string), which
7944
- // is why this went undetected until a non-default config-dir name
7945
- // was exercised.
7946
- content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7947
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7948
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7949
- content = content.replace(/~\/\.qwen\//g, pathPrefix);
7950
- content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
7951
- content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
7952
- content = content.replace(/~\/\.hermes\//g, pathPrefix);
7953
- content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
7954
- content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
7955
- // #3544: restore @-file-reference lines to the tilde form Claude Code
7956
- // actually expands — the SAME correction #3133 already applies to
7957
- // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
7958
- // restoreClaudeGlobalAtRefTilde's doc comment in
7959
- // runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
7960
- // emit path, which never had it: every @~/.claude/gsd-core/… include
7961
- // in a global install's workflows/references tree silently resolved
7962
- // to nothing (54 includes across 22 files on a live install).
7963
- if (runtime === 'claude') {
7964
- content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
7965
- }
7979
+ // #4377: with a project-relative prefix, mask `${VAR:-default}` shell
7980
+ // defaults out of the substitutions below and restore them after. The
7981
+ // runtime launcher snippet probes gsd-tools through a chain of those
7982
+ // (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/...`, one per
7983
+ // runtime); they are shell word expansions, not markdown @ includes,
7984
+ // and a relative value there resolves against the shell's cwd instead
7985
+ // of the project. Swapping an include that points at the wrong
7986
+ // checkout for a path that points at nothing is not a fix, and the
7987
+ // launcher already probes `$(git rev-parse --show-toplevel)/.claude`
7988
+ // first, so the multi-worktree case is handled before these defaults
7989
+ // are ever reached. The shared helper is the single owner of the
7990
+ // balanced masking grammar used by this path and the rewrite engine.
7991
+ const rewriteGenericPaths = (body) => {
7992
+ content = body;
7993
+ const globalClaudeRegex = /~\/\.claude\//g;
7994
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
7995
+ const localClaudeRegex = /\.\/\.claude\//g;
7996
+ content = content.replace(globalClaudeRegex, pathPrefix);
7997
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
7998
+ content = content.replace(localClaudeRegex, `./${dirName}/`);
7999
+ // #3544 review (Finding 1 fallout): guarded with the SAME
8000
+ // negative-lookahead convention already used at ~:2859-2860 below
8001
+ // ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
8002
+ // is satisfied by ANY non-word character, including '-' — so for a
8003
+ // --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
8004
+ // pathPrefix '$HOME/.claude-work/'), this pass re-matched the
8005
+ // '$HOME/.claude' PREFIX of its own slash-form output (lines above)
8006
+ // and re-appended the full prefix, corrupting every emitted path to
8007
+ // '$HOME/.claude-work-work/...'. Harmless no-op for the literal
8008
+ // default '.claude' (self-replace with an identical string), which
8009
+ // is why this went undetected until a non-default config-dir name
8010
+ // was exercised.
8011
+ content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
8012
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
8013
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
8014
+ content = content.replace(/~\/\.qwen\//g, pathPrefix);
8015
+ content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
8016
+ content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
8017
+ content = content.replace(/~\/\.hermes\//g, pathPrefix);
8018
+ content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
8019
+ content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
8020
+ // #3544: restore @-file-reference lines to the tilde form Claude Code
8021
+ // actually expands — the SAME correction #3133 already applies to
8022
+ // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
8023
+ // restoreClaudeGlobalAtRefTilde's doc comment in
8024
+ // runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
8025
+ // emit path, which never had it: every @~/.claude/gsd-core/… include
8026
+ // in a global install's workflows/references tree silently resolved
8027
+ // to nothing (54 includes across 22 files on a live install).
8028
+ if (runtime === 'claude') {
8029
+ content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
8030
+ }
8031
+ return content;
8032
+ };
8033
+ content = runtimeArtifactConversion._isRelativePathPrefix(pathPrefix)
8034
+ ? runtimeArtifactConversion._withShellDefaultsPreserved(content, rewriteGenericPaths)
8035
+ : rewriteGenericPaths(content);
7966
8036
  }
7967
8037
  content = processAttribution(content, getCommitAttribution(runtime));
7968
8038
 
@@ -8478,6 +8548,16 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8478
8548
  console.log(` ${green}✓${reset} Removed managed Codex ${eventName} hook from hooks.json`);
8479
8549
  }
8480
8550
  }
8551
+ // #2586: uninstall's own symmetric half of the orphaned-script cleanup —
8552
+ // same GSD-owned + unreferenced gate as the install-time call.
8553
+ const uninstallMonitorCleanup = hooksSurface.cleanupOrphanedCodexContextMonitorScript(targetDir);
8554
+ for (const deletedPath of uninstallMonitorCleanup.deleted) {
8555
+ removedCount++;
8556
+ console.log(` ${green}✓${reset} Removed orphaned Codex hook script (${path.basename(deletedPath)})`);
8557
+ }
8558
+ for (const warning of uninstallMonitorCleanup.warnings) {
8559
+ console.warn(` ${yellow}⚠${reset} Could not remove orphaned Codex hook script ${warning.path}: ${warning.reason}`);
8560
+ }
8481
8561
  }
8482
8562
 
8483
8563
  // 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
@@ -9891,6 +9971,11 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9891
9971
  // it from the directory it happened to be found in (#2872).
9892
9972
  runtime,
9893
9973
  scope: resolvedScope,
9974
+ // #4377: a surface re-apply is a separate process and cannot rely on the
9975
+ // installer's environment. Persist only a safe project-relative prefix.
9976
+ relativeIncludePrefix: resolvedScope === 'local' && hasRelativeIncludes
9977
+ ? runtimeArtifactConversion._projectRelativePrefixFromProjectRoot(process.cwd(), configDir)
9978
+ : undefined,
9894
9979
  files: {},
9895
9980
  };
9896
9981
 
@@ -10153,6 +10238,86 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
10153
10238
  return written;
10154
10239
  }
10155
10240
 
10241
+ /**
10242
+ * #4145: recover a pristine baseline from a hash-matching orphan stored at an
10243
+ * unexpected path under gsd-pristine/ (e.g. without the gsd-core/ prefix an
10244
+ * earlier release's writer dropped).
10245
+ *
10246
+ * The preserve-check's strict join (pristineDir + manifest-keyed relPath)
10247
+ * misses such snapshots, so they were pushed into regeneration from the
10248
+ * incoming release — and when the file changed upstream, the candidate's hash
10249
+ * could never satisfy the recorded outgoing hash, leaving the correct
10250
+ * baseline permanently unconsumed and unpruned (the self-perpetuating state
10251
+ * #4145 reports). Hash equality with pristine_hashes is the same authority
10252
+ * the #3657 drift guard trusts, so an exact match cannot be the wrong
10253
+ * baseline no matter where under gsd-pristine/ it lives.
10254
+ *
10255
+ * Recovery = relocation: copy the orphan to the canonical manifest-keyed path
10256
+ * (hash-verified after the copy) and remove the orphan only once the
10257
+ * canonical copy is verified in place. Returns true when the canonical path
10258
+ * ended up holding recorded-hash bytes. Never deletes anything it cannot
10259
+ * vouch for by hash, and never consumes a path that is the canonical path of
10260
+ * ANY manifest file (see canonicalSkip below) — only genuine orphans, which
10261
+ * no strict-join reader ever consults, are eligible for removal.
10262
+ */
10263
+ function recoverOrphanedPristine(pristineDir, relPath, recordedHash, canonicalSkip) {
10264
+ if (!recordedHash) return false;
10265
+ let orphanRel;
10266
+ try {
10267
+ // canonicalSkip = the normalized manifest keys: a file already sitting at
10268
+ // any canonical path can never be (re-)adopted through the scan. Without
10269
+ // this, two modified files sharing byte-identical outgoing content would
10270
+ // repeatedly "rescue" each other's canonical away (relocate + delete at
10271
+ // its home path) in alternating updates — bytes identical, state unstable.
10272
+ // It also keeps drift (#3657) / stale (#3407) territory with the caller.
10273
+ orphanRel = gsdFindPristineByHash(pristineDir, recordedHash, canonicalSkip);
10274
+ } catch {
10275
+ return false;
10276
+ }
10277
+ if (!orphanRel) return false;
10278
+ const outRef = resolveInstallRelativePath(pristineDir, relPath);
10279
+ if (!outRef) return false;
10280
+ try {
10281
+ fs.mkdirSync(path.dirname(outRef.fullPath), { recursive: true });
10282
+ fs.copyFileSync(path.join(pristineDir, orphanRel), outRef.fullPath);
10283
+ // Verify the relocated copy before removing the orphan — only a
10284
+ // hash-matching canonical counts as recovered.
10285
+ if (fileHash(outRef.fullPath) !== recordedHash) {
10286
+ try { fs.rmSync(outRef.fullPath, { force: true }); } catch { /* best-effort */ }
10287
+ return false;
10288
+ }
10289
+ // Orphan removal is best-effort: the canonical copy is already verified,
10290
+ // so a failed unlink leaves a harmless duplicate, never data loss.
10291
+ try { fs.rmSync(path.join(pristineDir, orphanRel), { force: true }); } catch { /* best-effort */ }
10292
+ return true;
10293
+ } catch {
10294
+ return false;
10295
+ }
10296
+ }
10297
+
10298
+ /**
10299
+ * #4135: honest N-of-M accounting for gsd-pristine/ baselines after an
10300
+ * update. The #3407 promotion rule keeps only hash-validated candidates
10301
+ * (byte-identical across the version span), so a multi-version update
10302
+ * legitimately ends with near-zero baselines — the collapse itself is NOT a
10303
+ * bug to hide; hiding it is. This renders the covered-of-total line the
10304
+ * update output prints either way, so "1 of 13" can never present like a
10305
+ * fully-covered run. Pure function (typed return) so tests lock the exact
10306
+ * contract without matching console prose.
10307
+ */
10308
+ function describeBaselineCoverage(totalModified, covered) {
10309
+ const total = Math.max(0, totalModified);
10310
+ const have = Math.min(Math.max(0, covered), total);
10311
+ const uncovered = total - have;
10312
+ return {
10313
+ complete: uncovered === 0,
10314
+ uncovered,
10315
+ text: uncovered === 0
10316
+ ? `gsd-pristine/ baselines cover ${have} of ${total} modified file(s)`
10317
+ : `gsd-pristine/ baselines cover ${have} of ${total} modified file(s) — ${uncovered} will be reported no_baseline by the reapply verifier`,
10318
+ };
10319
+ }
10320
+
10156
10321
  /**
10157
10322
  * Detect user-modified GSD files by comparing against install manifest.
10158
10323
  * Backs up modified files to gsd-local-patches/ for reapply after update.
@@ -10299,6 +10464,15 @@ function saveLocalPatches(configDir, pristineCtx) {
10299
10464
  const stalePaths = new Set();
10300
10465
  // Track which relPaths were successfully regenerated (from either missing or stale).
10301
10466
  const regeneratedPaths = new Set();
10467
+ // #4145: track which relPaths were recovered by relocating a hash-matching
10468
+ // orphan (stored at an unexpected path, e.g. without the gsd-core/ prefix).
10469
+ const rescuedPaths = new Set();
10470
+ // #4145: the set of paths that are SOME file's canonical pristine path
10471
+ // (every normalized manifest key). The orphan scan must never consume
10472
+ // these — see recoverOrphanedPristine.
10473
+ const canonicalSkip = new Set(
10474
+ Object.keys(manifest.files || {}).map((k) => normalizeInstallRelativePath(k)).filter(Boolean),
10475
+ );
10302
10476
  const missingPaths = [];
10303
10477
  for (const relPath of modified) {
10304
10478
  const outRef = resolveInstallRelativePath(pristineDir, relPath);
@@ -10320,6 +10494,17 @@ function saveLocalPatches(configDir, pristineCtx) {
10320
10494
  stalePaths.add(relPath);
10321
10495
  }
10322
10496
  }
10497
+ // #4145: canonical absent (or just removed as stale) — before falling
10498
+ // into regeneration, try to recover the baseline from a hash-matching
10499
+ // orphan elsewhere under gsd-pristine/ and relocate it to the canonical
10500
+ // path. This is the self-heal for snapshots an earlier release stored
10501
+ // without the gsd-core/ prefix: without it the state repeats forever
10502
+ // (regeneration candidates from the incoming release can never satisfy
10503
+ // the recorded outgoing hash when upstream changed the file).
10504
+ if (recoverOrphanedPristine(pristineDir, relPath, pristineHashes[relPath], canonicalSkip)) {
10505
+ rescuedPaths.add(relPath);
10506
+ continue;
10507
+ }
10323
10508
  // File absent from gsd-pristine/ (or just removed above as stale):
10324
10509
  // attempt hash-validated regeneration from new-release source.
10325
10510
  missingPaths.push(relPath);
@@ -10362,19 +10547,38 @@ function saveLocalPatches(configDir, pristineCtx) {
10362
10547
  }
10363
10548
  // `regenerated` = total files successfully regenerated (from missing OR stale).
10364
10549
  const regenerated = regeneratedPaths.size;
10550
+ // `rescued` = files recovered by relocating a hash-matching orphan to its
10551
+ // canonical path (#4145) — distinct from preservation (canonical already
10552
+ // correct) and regeneration (bytes re-derived from new-release source).
10553
+ const rescued = rescuedPaths.size;
10365
10554
  // `removed` = stale entries that were deleted and NOT subsequently regenerated.
10366
10555
  // Entries that were stale-deleted but then successfully regenerated are counted
10367
- // only in `regenerated` — the counts are non-overlapping.
10368
- const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p)).length;
10556
+ // only in `regenerated`; stale-deleted-then-orphan-rescued entries are counted
10557
+ // only in `rescued` — the counts are non-overlapping.
10558
+ const removed = [...stalePaths].filter(p => !regeneratedPaths.has(p) && !rescuedPaths.has(p)).length;
10369
10559
  if (preserved > 0) {
10370
10560
  console.log(' ' + green + '✓' + reset + ' Preserved ' + cyan + 'gsd-pristine/' + reset + ' (' + preserved + ' file(s)) for three-way merge');
10371
10561
  }
10562
+ if (rescued > 0) {
10563
+ console.log(' ' + green + '✓' + reset + ' Recovered ' + cyan + 'gsd-pristine/' + reset + ' (' + rescued + ' file(s)) by recorded hash from a legacy-path snapshot and relocated them (#4145)');
10564
+ }
10372
10565
  if (regenerated > 0) {
10373
10566
  console.log(' ' + green + '✓' + reset + ' Regenerated ' + cyan + 'gsd-pristine/' + reset + ' (' + regenerated + ' file(s)) via hash-validated new-release source');
10374
10567
  }
10375
10568
  if (removed > 0) {
10376
10569
  console.log(' ' + yellow + 'i' + reset + ' Removed ' + removed + ' stale gsd-pristine/ snapshot(s); regenerated ' + regenerated + ' of those — falls back to over-broad verify heuristic for the rest');
10377
10570
  }
10571
+ // #4135: the honest N-of-M coverage line. Preserved/rescued/regenerated
10572
+ // are disjoint buckets (see their accounting comments above), so their
10573
+ // sum is exactly the files that ended this update with a hash-valid
10574
+ // baseline. A partial result renders as an info line, not an error:
10575
+ // the collapse is legitimate (#3407), hiding it was the bug.
10576
+ const coverage = describeBaselineCoverage(modified.length, preserved + rescued + regenerated);
10577
+ if (coverage.complete) {
10578
+ console.log(' ' + green + '✓' + reset + ' ' + coverage.text);
10579
+ } else {
10580
+ console.log(' ' + yellow + 'i' + reset + ' ' + coverage.text);
10581
+ }
10378
10582
  }
10379
10583
  }
10380
10584
  return modified;
@@ -10655,6 +10859,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10655
10859
  isWindowsHost,
10656
10860
  resolvedTarget,
10657
10861
  homeDir,
10862
+ // #4377: the runtime's own localConfigDir. This is the prefix that reaches
10863
+ // copyWithPathReplacement, i.e. the one actually written into every
10864
+ // emitted command/skill/workflow body — the rewrite-engine seams below
10865
+ // handle re-applied surfaces, not the first install.
10866
+ localDirName: _hostBehaviors(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
10658
10867
  });
10659
10868
 
10660
10869
  // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
@@ -10665,6 +10874,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10665
10874
 
10666
10875
  // Track installation failures
10667
10876
  const failures = [];
10877
+ const configuredEntrypoints = [];
10668
10878
  let installerMigrationResult = null;
10669
10879
  const rollbackInstallerMigrations = () => {
10670
10880
  if (!installerMigrationResult || typeof installerMigrationResult.rollback !== 'function') return;
@@ -10717,7 +10927,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10717
10927
  // Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
10718
10928
  const codexPreInstallAgentContents = new Map();
10719
10929
  let codexPreInstallVersionBytes = null;
10720
- if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
10930
+ // #4544 — manifest-driven snapshot state (captured in the block below):
10931
+ // codexPreInstallManagedFiles — Map<normalizedRelPath, Buffer|null>; one
10932
+ // entry per path the PRIOR install's gsd-file-manifest.json recorded.
10933
+ // null means the path did not exist pre-install, so rollback re-deletes
10934
+ // whatever this install put there instead of resurrecting it.
10935
+ // codexPreInstallManifestBytes — Buffer (or null) of the prior manifest file
10936
+ // itself, which a reinstall rewrites.
10937
+ // codexPreInstallHooksTree — Map<relPath, Buffer>, a full recursive
10938
+ // snapshot of <targetDir>/hooks/. The Codex manifest deliberately omits
10939
+ // hooks/ (the !isCodex gate on shared-hooks tracking), and hooks/ is
10940
+ // shared space, so the restore is wholesale: user files that predate
10941
+ // the install are in the snapshot and come back; anything the failed
10942
+ // install staged does not.
10943
+ const codexPreInstallManagedFiles = new Map();
10944
+ let codexPreInstallManifestBytes = null;
10945
+ const codexPreInstallHooksTree = new Map();
10946
+ // #4544 (review) — capture-state flags the restore must consult:
10947
+ // codexManagedSnapshotCaptured — the capture gate ran at all. When
10948
+ // false (non-Codex runtimes, minimal mode) NO pre-install state was
10949
+ // recorded, and the only safe restore is no restore: an empty
10950
+ // snapshot must never be read as "hooks/ was absent".
10951
+ // codexPreInstallHooksDirPreExisted — hooks/ existed as a DIRECTORY
10952
+ // pre-install. A pre-existing hooks FILE is left alone on rollback
10953
+ // rather than deleted.
10954
+ // codexPreInstallHooksCaptureIncomplete — some part of the hooks/ tree
10955
+ // could not be read (permissions, special files). The restore
10956
+ // downgrades to per-file so an uncapturable user file is never
10957
+ // destroyed by a wholesale delete whose snapshot lacked it.
10958
+ let codexManagedSnapshotCaptured = false;
10959
+ // null = the gate never ran; true/false = the gate ran and hooks/ (did|did
10960
+ // not) exist as a directory pre-install. Two states are load-bearing: a
10961
+ // clean first install records false, so its rollback removes the staged
10962
+ // hooks/ tree entirely; a non-Codex runtime records null, so rollback does
10963
+ // nothing.
10964
+ let codexPreInstallHooksDirPreExisted = null;
10965
+ let codexPreInstallHooksCaptureIncomplete = false;
10966
+ // #4249 CR: not gated on install mode. restoreCodexSnapshot is reachable for
10967
+ // a core/--minimal install too (#2695), and its pass-2 sweeps remove every
10968
+ // gsd-* skill dir / agent file the snapshot does not claim — so an empty
10969
+ // minimal-mode snapshot deleted the whole surface with nothing to restore.
10970
+ if (_hostBehaviors(runtime).tomlConfigInstall) {
10971
+ codexManagedSnapshotCaptured = true;
10721
10972
  const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
10722
10973
  if (fs.existsSync(_preSkillsDir)) {
10723
10974
  for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
@@ -10759,18 +11010,212 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10759
11010
  if (fs.existsSync(_preVersionPath)) {
10760
11011
  try { codexPreInstallVersionBytes = fs.readFileSync(_preVersionPath); } catch (_) { /* best-effort */ }
10761
11012
  }
10762
- }
11013
+ // #4544 — capture the manifest-driven surfaces, same best-effort
11014
+ // conventions as the skills/ snapshot above. readInstallManifest is the
11015
+ // same hardened reader installer-migrations uses (array/ garbage shapes
11016
+ // degrade to an empty file set — rollback then simply covers less, never
11017
+ // crashes), and resolveInstallRelativePath keeps a hostile manifest key
11018
+ // from turning into a write outside the install root.
11019
+ const _priorManifest = readInstallManifest(targetDir);
11020
+ for (const rel of Object.keys(_priorManifest.files)) {
11021
+ const resolved = resolveInstallRelativePath(targetDir, rel);
11022
+ if (!resolved) continue;
11023
+ try {
11024
+ codexPreInstallManagedFiles.set(resolved.relPath, fs.readFileSync(resolved.fullPath));
11025
+ } catch (_) {
11026
+ // Listed but absent/unreadable pre-install: snapshot absence, so
11027
+ // rollback re-deletes instead of resurrecting.
11028
+ codexPreInstallManagedFiles.set(resolved.relPath, null);
11029
+ }
11030
+ }
11031
+ const _preManifestPath = path.join(targetDir, MANIFEST_NAME);
11032
+ if (fs.existsSync(_preManifestPath)) {
11033
+ try { codexPreInstallManifestBytes = fs.readFileSync(_preManifestPath); } catch (_) { /* best-effort */ }
11034
+ }
11035
+ // #4544 (review) — a clean FIRST install has no prior manifest, so nothing
11036
+ // above records the payload this install is about to write, and a failed
11037
+ // clean install would roll back to a half-written tree. Enumerate the SAME
11038
+ // source directories the installer copies (a directory walk tracks the
11039
+ // source tree automatically — no second file list to keep in parity) and
11040
+ // record every path as absent-pre-install. On a reinstall most of these
11041
+ // already carry entries from the prior manifest; any that do not (files
11042
+ // new in this version) snapshot their pre-install bytes or absence exactly
11043
+ // like the rest, which also closes the new-version-file residual.
11044
+ const _recordWritePlanTree = (srcDir, relPrefix) => {
11045
+ let children;
11046
+ try { children = fs.readdirSync(srcDir, { withFileTypes: true }); } catch (_) { return; }
11047
+ for (const child of children) {
11048
+ const rel = relPrefix ? `${relPrefix}/${child.name}` : child.name;
11049
+ if (child.isDirectory()) {
11050
+ _recordWritePlanTree(path.join(srcDir, child.name), rel);
11051
+ } else if (child.isFile()) {
11052
+ if (codexPreInstallManagedFiles.has(rel)) continue;
11053
+ // USER_OWNED_ARTIFACTS are manifest-relative to gsd-core/ (#2771):
11054
+ // they are durably staged across reinstalls and must never enter a
11055
+ // rollback delete-set.
11056
+ const manifestRel = rel.startsWith('gsd-core/') ? rel.slice('gsd-core/'.length) : rel;
11057
+ if (USER_OWNED_ARTIFACTS.includes(manifestRel)) continue;
11058
+ const resolved = resolveInstallRelativePath(targetDir, rel);
11059
+ if (!resolved) continue;
11060
+ try {
11061
+ codexPreInstallManagedFiles.set(rel, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
11062
+ } catch (_) {
11063
+ codexPreInstallManagedFiles.set(rel, null);
11064
+ }
11065
+ }
11066
+ }
11067
+ };
11068
+ _recordWritePlanTree(path.join(src, 'gsd-core'), 'gsd-core');
11069
+ _recordWritePlanTree(path.join(src, 'scripts', 'changeset'), 'scripts/changeset');
11070
+ _recordWritePlanTree(path.join(src, 'scripts', 'lib'), 'scripts/lib');
11071
+ // gsd-core/CHANGELOG.md is sourced from the repo root (not src/gsd-core)
11072
+ // and gsd-core/.gsd-runtime is generated at install time — neither appears
11073
+ // in the directory walks, so record them explicitly.
11074
+ for (const standalone of ['gsd-core/CHANGELOG.md', 'gsd-core/.gsd-runtime', 'scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs']) {
11075
+ if (codexPreInstallManagedFiles.has(standalone)) continue;
11076
+ const resolved = resolveInstallRelativePath(targetDir, standalone);
11077
+ if (!resolved) continue;
11078
+ try {
11079
+ codexPreInstallManagedFiles.set(standalone, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
11080
+ } catch (_) {
11081
+ codexPreInstallManagedFiles.set(standalone, null);
11082
+ }
11083
+ }
11084
+ // hooks/ — full recursive snapshot, but never blind: lstat every entry so
11085
+ // a symlink under hooks/ is neither followed (a link to a FIFO would hang
11086
+ // the installer, /dev/zero would exhaust memory, and a link to private
11087
+ // data would copy that data into the snapshot — hooks/ is user-writable
11088
+ // shared space and, for local installs, repo-controllable) nor restored
11089
+ // as a link. Anything unreadable or special marks the capture INCOMPLETE
11090
+ // so the restore downgrades to per-file instead of wholesale-deleting a
11091
+ // tree it never fully saw. A pre-existing hooks FILE (not directory) is
11092
+ // recorded as such and left alone on rollback.
11093
+ const _preHooksPath = path.join(targetDir, 'hooks');
11094
+ let _preHooksStat = null;
11095
+ try { _preHooksStat = fs.lstatSync(_preHooksPath); } catch (_) { /* absent */ }
11096
+ codexPreInstallHooksDirPreExisted = Boolean(_preHooksStat && _preHooksStat.isDirectory());
11097
+ if (codexPreInstallHooksDirPreExisted) {
11098
+ const _snapshotHooksDir = (dir, relBase) => {
11099
+ let children;
11100
+ try { children = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) {
11101
+ codexPreInstallHooksCaptureIncomplete = true;
11102
+ return;
11103
+ }
11104
+ for (const child of children) {
11105
+ const relPath = relBase ? `${relBase}/${child.name}` : child.name;
11106
+ const fullPath = path.join(dir, child.name);
11107
+ let st = null;
11108
+ try { st = fs.lstatSync(fullPath); } catch (_) {
11109
+ codexPreInstallHooksCaptureIncomplete = true;
11110
+ continue;
11111
+ }
11112
+ if (st.isDirectory()) {
11113
+ _snapshotHooksDir(fullPath, relPath);
11114
+ } else if (st.isFile()) {
11115
+ try { codexPreInstallHooksTree.set(relPath, fs.readFileSync(fullPath)); } catch (_) {
11116
+ codexPreInstallHooksCaptureIncomplete = true;
11117
+ }
11118
+ } else {
11119
+ codexPreInstallHooksCaptureIncomplete = true;
11120
+ }
11121
+ }
11122
+ };
11123
+ _snapshotHooksDir(_preHooksPath, '');
11124
+ }
11125
+ }
11126
+
11127
+ // #4544 — shared restore for the manifest-driven surfaces. Called by BOTH
11128
+ // rollback paths: _codexPreConfigRollback (CHANGELOG.md, scripts/, the
11129
+ // initial manifest write AND — via installer migrations' stale-hook removal
11130
+ // — hooks/ itself are all mutated BEFORE config.toml is touched, so the
11131
+ // early path must cover them) and the full restoreCodexSnapshot() below.
11132
+ // Best-effort throughout, matching the #3245 convention: restore failures
11133
+ // never mask the original install error.
11134
+ const restoreCodexManagedSnapshot = () => {
11135
+ // #4544 (review) — if the capture never ran (non-Codex runtimes, minimal
11136
+ // mode), no pre-install state was recorded. The only safe action is NONE:
11137
+ // an empty snapshot must never be read as "hooks/ was absent", or a
11138
+ // minimal-mode rollback would delete the user's entire hooks/ tree.
11139
+ if (!codexManagedSnapshotCaptured) return;
11140
+ // hooks/ — the pre-install tree is restored wholesale: a user file that
11141
+ // predated the install is IN the snapshot and comes back; anything the
11142
+ // failed install staged is not, and goes away with the tree. When the
11143
+ // capture was INCOMPLETE, wholesale deletion would permanently destroy a
11144
+ // file whose bytes were never captured, so the restore downgrades to
11145
+ // per-file: put back what was captured and remove only the names GSD
11146
+ // itself stages (the hoisted CODEX_HOOKS_TO_COPY set plus the CommonJS
11147
+ // marker). hooks/lib/ is left untouched in that mode — its contents are
11148
+ // transitive and cannot be enumerated safely without the capture.
11149
+ if (codexPreInstallHooksDirPreExisted !== null) {
11150
+ const _hooksRestoreDir = path.join(targetDir, 'hooks');
11151
+ if (!codexPreInstallHooksDirPreExisted) {
11152
+ // Clean first install: nothing pre-existed under hooks/, so nothing
11153
+ // the failed install staged may survive either.
11154
+ try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
11155
+ } else if (!codexPreInstallHooksCaptureIncomplete) {
11156
+ try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
11157
+ for (const [relPath, buf] of codexPreInstallHooksTree) {
11158
+ const destFile = path.join(_hooksRestoreDir, relPath);
11159
+ try {
11160
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
11161
+ fs.writeFileSync(destFile, buf);
11162
+ } catch (_) { /* best-effort */ }
11163
+ }
11164
+ } else {
11165
+ // GSD-owned names are removed FIRST: several of them are also
11166
+ // legitimate pre-install files the snapshot just restored, and a
11167
+ // removal pass after the restore would delete the restored bytes.
11168
+ for (const hookName of CODEX_HOOKS_TO_COPY) {
11169
+ try { fs.rmSync(path.join(_hooksRestoreDir, hookName), { force: true }); } catch (_) { /* best-effort */ }
11170
+ }
11171
+ try { fs.rmSync(path.join(_hooksRestoreDir, 'package.json'), { force: true }); } catch (_) { /* best-effort */ }
11172
+ for (const [relPath, buf] of codexPreInstallHooksTree) {
11173
+ const destFile = path.join(_hooksRestoreDir, relPath);
11174
+ try {
11175
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
11176
+ fs.writeFileSync(destFile, buf);
11177
+ } catch (_) { /* best-effort */ }
11178
+ }
11179
+ }
11180
+ }
11181
+ // Every GSD-owned path the prior manifest recorded (plus the clean-install
11182
+ // write plan): restore bytes, or re-delete a path that was absent
11183
+ // pre-install.
11184
+ for (const [relPath, buf] of codexPreInstallManagedFiles) {
11185
+ const resolved = resolveInstallRelativePath(targetDir, relPath);
11186
+ if (!resolved) continue;
11187
+ try {
11188
+ if (buf !== null) {
11189
+ fs.mkdirSync(path.dirname(resolved.fullPath), { recursive: true });
11190
+ fs.writeFileSync(resolved.fullPath, buf);
11191
+ } else if (fs.existsSync(resolved.fullPath)) {
11192
+ fs.rmSync(resolved.fullPath, { force: true });
11193
+ }
11194
+ } catch (_) { /* best-effort */ }
11195
+ }
11196
+ // The prior manifest file itself: reinstall rewrites it; rollback returns
11197
+ // the previous install's manifest (or removes it on a clean first install).
11198
+ const _manifestRestorePath = path.join(targetDir, MANIFEST_NAME);
11199
+ if (codexPreInstallManifestBytes !== null) {
11200
+ try { fs.writeFileSync(_manifestRestorePath, codexPreInstallManifestBytes); } catch (_) { /* best-effort */ }
11201
+ } else if (fs.existsSync(_manifestRestorePath)) {
11202
+ try { fs.unlinkSync(_manifestRestorePath); } catch (_) { /* best-effort */ }
11203
+ }
11204
+ };
10763
11205
 
10764
11206
  // #3245 CR finding 2 — Rollback coverage extends to ALL post-snapshot operations,
10765
11207
  // not just the Codex config/hook error paths. Any throw between snapshot capture and
10766
11208
  // the Codex config block (skills copy, agents copy, VERSION write, manifest write, etc.)
10767
11209
  // must also trigger rollback so the caller is never left in a partially-installed state.
10768
11210
  //
10769
- // _codexPreConfigRollback covers the four surfaces that can be mutated before
10770
- // config.toml is touched: skills/, agents/, gsd-core/VERSION, and orphaned
11211
+ // _codexPreConfigRollback covers the surfaces that can be mutated before
11212
+ // config.toml is touched: skills/, agents/, gsd-core/VERSION, the manifest-
11213
+ // driven surfaces (#4544 — CHANGELOG.md, scripts/, .gsd-runtime and the
11214
+ // manifest itself are all rewritten in this window), and orphaned
10771
11215
  // atomic-write temp files. It is safe to call before any writes have happened.
10772
11216
  // The full restoreCodexSnapshot() (defined inside the config block) additionally
10773
- // handles config.toml, which is not yet touched at this point in the pipeline.
11217
+ // handles config.toml and the staged hooks/ tree, which are not yet touched
11218
+ // at this point in the pipeline.
10774
11219
  const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
10775
11220
  rollbackInstallerMigrations();
10776
11221
  // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
@@ -10831,6 +11276,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10831
11276
  } else if (fs.existsSync(_earlyVersionPath)) {
10832
11277
  try { fs.unlinkSync(_earlyVersionPath); } catch (_) { /* best-effort */ }
10833
11278
  }
11279
+ // #4544 — manifest-driven surfaces (CHANGELOG.md, scripts/, the initial
11280
+ // manifest write, and — via installer migrations' stale-hook removal —
11281
+ // hooks/ itself are all mutated in this window). The shared restore is
11282
+ // also idempotent against an untouched tree.
11283
+ restoreCodexManagedSnapshot();
10834
11284
  // Orphaned atomic-write temp files.
10835
11285
  const _earlyTmpPattern = /\.tmp-\d+-\d+$/;
10836
11286
  function _earlyCleanTmpFiles(dir) {
@@ -11691,7 +12141,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11691
12141
  console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
11692
12142
  break;
11693
12143
  case 'preserved-foreign':
11694
- console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker) — GSD hooks may not resolve as CommonJS`);
12144
+ // #4759: the foreign file usually DOES declare "type": "commonjs" —
12145
+ // any hand-written or formatter-touched package.json does — and Node
12146
+ // then loads the staged .js hooks as CommonJS, so the old
12147
+ // unconditional "may not resolve" claim was usually false. The
12148
+ // sibling plugin path (src/install-engine.cts) words this same
12149
+ // outcome conditionally; match it and keep will-not-load conditional
12150
+ // on "type": "module", the only case where it is true.
12151
+ console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker). If it declares "type": "module", the staged hooks will not load.`);
11695
12152
  break;
11696
12153
  case 'failed':
11697
12154
  // Best-effort: a read-only or full config dir must not abort the
@@ -11923,6 +12380,42 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11923
12380
  manifestFiles = null;
11924
12381
  }
11925
12382
  if (manifestFiles !== null) {
12383
+ // #4667: codex-installed artifacts must not keep `@~/.claude/gsd-core/…`
12384
+ // include references — the `@` form resolves into the CLAUDE install
12385
+ // (wrong copy on dual-runtime machines at divergent versions, nothing at
12386
+ // all on codex-only ones; #570 cause 2 residue). Every target ships in
12387
+ // the codex install, so rewriting the `@~/` include form to the codex
12388
+ // root is mechanical and correct. This runs after all .md emitters
12389
+ // (several bypass the per-runtime converters — that is how the leak
12390
+ // survived the per-emitter fixes; the agent .tomls are generated later
12391
+ // and prefix themselves), and before the scan below, which stays as the
12392
+ // verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
12393
+ // fallback chains and prose `.claude` mentions carry no `@~/` prefix and
12394
+ // are deliberately untouched, as is CHANGELOG.md.
12395
+ if (runtime === 'codex') {
12396
+ for (const relPath of manifestFiles) {
12397
+ const fileName = path.basename(relPath);
12398
+ if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
12399
+ if (fileName === 'CHANGELOG.md') continue;
12400
+ const rewritePath = path.join(targetDir, relPath);
12401
+ let rewriteContent;
12402
+ try {
12403
+ rewriteContent = fs.readFileSync(rewritePath, 'utf8');
12404
+ } catch (rewriteErr) {
12405
+ continue; // inaccessible or missing — the scan below reports or skips it
12406
+ }
12407
+ const rewritten = rewriteContent
12408
+ .split('@~/.claude/gsd-core/').join('@~/.codex/gsd-core/')
12409
+ .split('@$HOME/.claude/gsd-core/').join('@$HOME/.codex/gsd-core/');
12410
+ if (rewritten !== rewriteContent) {
12411
+ try {
12412
+ fs.writeFileSync(rewritePath, rewritten);
12413
+ } catch (writeErr) {
12414
+ continue; // never fail the install over the rewrite; the scan still warns
12415
+ }
12416
+ }
12417
+ }
12418
+ }
11926
12419
  for (const relPath of manifestFiles) {
11927
12420
  const fileName = path.basename(relPath);
11928
12421
  if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
@@ -12131,6 +12624,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12131
12624
  try { fs.unlinkSync(_rollbackVersionPath); } catch (_) { /* best-effort */ }
12132
12625
  }
12133
12626
 
12627
+ // 4b. #4544 — manifest-driven surfaces: the staged hooks/ tree, every
12628
+ // GSD-owned path the prior manifest recorded (scripts/, gsd-core/
12629
+ // payload), and the prior manifest file itself.
12630
+ restoreCodexManagedSnapshot();
12631
+
12134
12632
  // 5. Orphaned atomic-write temp files (<file>.tmp-<pid>-<n>) in targetDir.
12135
12633
  // These can accumulate if an atomic write fails mid-rename. Best-effort scan.
12136
12634
  //
@@ -12197,12 +12695,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12197
12695
  // stageTransitiveHookLibs call after the copy loop. The #3579 boundary is
12198
12696
  // preserved: helpers no staged Codex hook requires (graphify tooling among
12199
12697
  // them) are still not shipped.
12200
- const CODEX_HOOKS_TO_COPY = [
12201
- 'gsd-check-update.js',
12202
- 'gsd-check-update-worker.js',
12203
- 'managed-hooks-registry.cjs',
12204
- 'gsd-context-monitor.js',
12205
- ];
12698
+ // #2586: gsd-context-monitor.js is deliberately NOT copied for Codex.
12699
+ // It reads the statusline bridge file (${TMPDIR}/claude-ctx-{session_id}.json)
12700
+ // written only by hooks/gsd-statusline.js, which Codex never installs — so
12701
+ // every registered event was a guaranteed silent no-op (readSentinel throws
12702
+ // ENOENT -> allow(undefined), every invocation, every event, no exceptions).
12703
+ // A pre-#2586 install's stale copy + hooks.json registrations are cleaned
12704
+ // up below (see the CODEX_EXTENDED_HOOK_EVENTS loop), not re-added here.
12705
+ // CODEX_HOOKS_TO_COPY itself lives at module scope (#4544) — the rollback's
12706
+ // incomplete-capture path must name the same set without a second literal.
12206
12707
  const codexHooksSrc = path.join(src, 'hooks', 'dist');
12207
12708
  if (fs.existsSync(codexHooksSrc)) {
12208
12709
  const codexHooksDest = path.join(targetDir, 'hooks');
@@ -12396,6 +12897,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12396
12897
  absoluteRunner: codexNodeRunner,
12397
12898
  platform: process.platform,
12398
12899
  });
12900
+ configuredEntrypoints.push(...(hookWrite.configuredEntrypoints || []));
12399
12901
  if (hookWrite.wrote) {
12400
12902
  console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart via hooks.json)`);
12401
12903
  } else {
@@ -12403,35 +12905,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12403
12905
  }
12404
12906
  }
12405
12907
 
12406
- // ── Codex extended hook events (#772, #2088) ─────────────────────────
12407
- // Codex CLI stabilised a full hook-event set in rust-v0.137.0. GSD
12408
- // registers CODEX_EXTENDED_HOOK_EVENTS (#2088 adds the 6 documented
12409
- // events beyond the original #772 three) — all routed through
12410
- // gsd-context-monitor.js so context-headroom warnings surface at each
12411
- // lifecycle point: SubagentStart/SubagentStop (subagent open/close),
12412
- // Stop (final-response), PreToolUse/PostToolUse (tool boundaries),
12413
- // PermissionRequest (approval prompts), Pre/PostCompact (context
12414
- // compaction), and UserPromptSubmit (per-turn context injection). The
12415
- // context-monitor script decides per-payload what to do; unregistered
12416
- // events simply never fire.
12417
- //
12418
- // Guard: only register when the context-monitor file exists and the node
12419
- // runner is available — same guards as the SessionStart path above.
12420
- const contextMonitorFile = path.join(targetDir, 'hooks', 'gsd-context-monitor.js');
12421
- if (codexNodeRunner && fs.existsSync(contextMonitorFile)) {
12422
- for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
12423
- const eventWrite = ensureCodexHooksJsonEvent(targetDir, codexEvent, {
12424
- absoluteRunner: codexNodeRunner,
12425
- platform: process.platform,
12426
- });
12427
- if (eventWrite.wrote) {
12428
- console.log(` ${green}✓${reset} Configured Codex hooks (${codexEvent} via hooks.json)`);
12429
- } else if (eventWrite.changed) {
12430
- console.log(` ${green}✓${reset} Verified Codex hooks (${codexEvent} via hooks.json)`);
12431
- }
12908
+ // #2586: Codex's hook payload carries no context/token-usage field
12909
+ // (confirmed against codex-rs/hooks/src/schema.rs), so agent-facing
12910
+ // context warnings and GSD phase/lifecycle display cannot be
12911
+ // supported on this runtime — state that plainly during install
12912
+ // rather than silently omitting the capability. Matches
12913
+ // capabilities/codex/capability.json's hostBehaviors.unsupportedFeatures.
12914
+ console.log(` ${dim}↳${reset} Codex: agent-facing context warnings and GSD phase/lifecycle display are unsupported (Codex's hook payload has no context-usage metric)`);
12915
+
12916
+ // ── Codex extended hook events (#772, #2088) — REMOVED by #2586 ──────
12917
+ // gsd-context-monitor.js is no longer copied or registered for Codex
12918
+ // (see the CODEX_HOOKS_TO_COPY comment above): every one of these
12919
+ // events was a guaranteed silent no-op, since the metrics bridge file
12920
+ // it reads is only ever written by Claude's own statusline hook.
12921
+ // Every event in CODEX_EXTENDED_HOOK_EVENTS is unconditionally
12922
+ // reconciled here — not gated on the script existing — so a
12923
+ // pre-#2586 install's stale registrations (exact current shape, or a
12924
+ // recognized legacy shape via isManagedHookCommand's
12925
+ // includeLegacyAliases) are stripped on reinstall. Mirrors the
12926
+ // unconditional uninstall-time loop over the same constant. A
12927
+ // registration whose command does not match the managed shape (a
12928
+ // hand-customized entry) survives untouched — see
12929
+ // reconcileCodexHooksJsonEvent's isManagedHookCommand filter.
12930
+ for (const codexEvent of CODEX_EXTENDED_HOOK_EVENTS) {
12931
+ const eventCleanup = removeCodexHooksJsonEvent(targetDir, codexEvent);
12932
+ if (eventCleanup.changed) {
12933
+ console.log(` ${green}✓${reset} Removed stale Codex ${codexEvent} context-monitor hook from hooks.json`);
12432
12934
  }
12433
- } else if (!codexNodeRunner) {
12434
- console.warn(` ${yellow}⚠${reset} Skipped Codex extended hook-event registration — Node runner unavailable.`);
12935
+ }
12936
+ // Delete the orphaned script (+ Windows .cmd shim) left by a
12937
+ // pre-#2586 install, but ONLY once no surviving hooks.json
12938
+ // registration under any event still references it, and only when
12939
+ // the on-disk file is GSD's own (see design doc's Ownership check —
12940
+ // a content-signature check, not manifest membership, so this works
12941
+ // on the very first reinstall after upgrading, with no bootstrap
12942
+ // gap). A deletion failure never reverts the (already safe,
12943
+ // already-written) hooks.json cleanup above — must-have #8.
12944
+ const monitorCleanup = hooksSurface.cleanupOrphanedCodexContextMonitorScript(targetDir);
12945
+ for (const deletedPath of monitorCleanup.deleted) {
12946
+ console.log(` ${green}✓${reset} Removed orphaned Codex hook script (${path.basename(deletedPath)})`);
12947
+ }
12948
+ for (const warning of monitorCleanup.warnings) {
12949
+ console.warn(` ${yellow}⚠${reset} Could not remove orphaned Codex hook script ${warning.path}: ${warning.reason}`);
12435
12950
  }
12436
12951
  // ── end Codex extended hook events ────────────────────────────────────
12437
12952
  }
@@ -12462,7 +12977,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12462
12977
  }
12463
12978
 
12464
12979
  persistActiveProfileMarker();
12465
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
12980
+ // #4249: expose restoreCodexSnapshot (#3245) as a SECOND, separately named
12981
+ // rollback rather than rebinding `rollbackInstallerMigrations` to it. A
12982
+ // configured-entrypoint validation failure discovered later (outside this
12983
+ // function, after Codex's own hooks.json/config.toml write already
12984
+ // succeeded) previously had only the installer-migrations closure to call,
12985
+ // leaving the just-written config.toml/hooks.json broken on disk despite
12986
+ // Codex already owning a full pre-install snapshot/restore for exactly this.
12987
+ //
12988
+ // Every runtime's `rollbackInstallerMigrations` therefore still means what
12989
+ // it says — the installer-migrations-only closure, which is what a
12990
+ // finalize-stage failure that is NOT an entrypoint-validation failure gets
12991
+ // (the Phase 4 contract). `rollbackPreInstallSnapshot` is Codex-only and is
12992
+ // chosen only for entrypoint-validation failures. See the selection in
12993
+ // installAllRuntimes' rollbackFinalizedInstallerMigrations.
12994
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations, rollbackPreInstallSnapshot: restoreCodexSnapshot };
12466
12995
  }
12467
12996
 
12468
12997
  if (plan.installSurface === 'copilot-instructions') {
@@ -12492,7 +13021,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12492
13021
  writeCopilotHookConfig(targetDir);
12493
13022
  console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`);
12494
13023
  persistActiveProfileMarker();
12495
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13024
+ // #4249: `[]`, not omitted — every Copilot hook is an inline `printf`
13025
+ // one-liner (GSD_COPILOT_*_HOOK_BASH/PWSH in src/runtime-hooks-surface.cts),
13026
+ // so this runtime genuinely launches no GSD-managed script and has no
13027
+ // interpreter to resolve. Stated explicitly like every other branch rather
13028
+ // than leaning on installAllRuntimes' `|| []` defence.
13029
+ // #4249 (antigravity review): `rollbackInstallerMigrations` was missing here
13030
+ // — every other branch returns it. This PR's own aggregate entrypoint gate
13031
+ // is what makes the gap reachable: an unrelated runtime's invalid entrypoint
13032
+ // now triggers rollbackFinalizedInstallerMigrations for every result in the
13033
+ // batch, and a Copilot result with no rollback function silently skips
13034
+ // reverting Copilot's own installer migrations.
13035
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
12496
13036
  }
12497
13037
 
12498
13038
  if (plan.installSurface === 'cursor-hooks-json') {
@@ -12515,7 +13055,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12515
13055
  // The re-run is retained for parity with the settings.json install path.
12516
13056
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
12517
13057
  persistActiveProfileMarker();
12518
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13058
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: cursorHookResult.configuredEntrypoints, rollbackInstallerMigrations };
12519
13059
  }
12520
13060
 
12521
13061
  if (plan.installSurface === 'profile-marker-only') {
@@ -12571,6 +13111,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12571
13111
  const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
12572
13112
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
12573
13113
  const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
13114
+ configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
12574
13115
  if (kimiHooksResult.changed) {
12575
13116
  console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
12576
13117
  }
@@ -12627,6 +13168,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12627
13168
  const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
12628
13169
  platform: process.platform,
12629
13170
  });
13171
+ configuredEntrypoints.push(...windsurfHookResult.configuredEntrypoints);
12630
13172
  if (windsurfHookResult.changed) {
12631
13173
  console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
12632
13174
  } else {
@@ -12642,19 +13184,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12642
13184
  }
12643
13185
 
12644
13186
  persistActiveProfileMarker();
12645
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13187
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations };
12646
13188
  }
12647
13189
 
12648
13190
  if (plan.installSurface === 'cline-rules') {
12649
13191
  // Cline uses the `.clinerules/` directory form (issue #787): GSD rules live
12650
13192
  // at .clinerules/gsd.md and a PreToolUse lifecycle hook at
12651
13193
  // .clinerules/hooks/PreToolUse. Global installs also get ~/.agents/AGENTS.md.
12652
- writeClineArtifacts(targetDir, isGlobal);
13194
+ const clineArtifacts = writeClineArtifacts(targetDir, isGlobal);
12653
13195
  // Re-run the manifest pass: these artifacts are written *after* the earlier
12654
13196
  // writeManifest() call, so a second pass is needed to hash-track them.
12655
13197
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
12656
13198
  persistActiveProfileMarker();
12657
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13199
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: clineArtifacts.configuredEntrypoints, rollbackInstallerMigrations };
12658
13200
  }
12659
13201
 
12660
13202
  // Configure statusline and hooks in settings.json (or settings.local.json for local Claude installs).
@@ -12779,8 +13321,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12779
13321
  persistActiveProfileMarker();
12780
13322
  // Callers index this result by `runtime` (installAllRuntimes' statusline
12781
13323
  // lookup), so every early exit must return the full shape — a bare return
12782
- // crashes the install rather than skipping one file.
12783
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13324
+ // crashes the install rather than skipping one file. That includes
13325
+ // configuredEntrypoints/rollbackInstallerMigrations: rollbackFinalizedInstallerMigrations
13326
+ // reads result.rollbackInstallerMigrations unconditionally, and an omitted
13327
+ // field there silently skips this runtime's rollback on a finalize-stage failure.
13328
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
12784
13329
  }
12785
13330
  const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
12786
13331
  // #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
@@ -12802,7 +13347,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12802
13347
  // runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
12803
13348
  // check inside projectLocalHookPrefix.
12804
13349
  const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
12805
- const hookOpts = { portableHooks: hasPortableHooks, runtime };
13350
+ const settingsEntrypoints = [];
13351
+ const hookOpts = {
13352
+ portableHooks: hasPortableHooks,
13353
+ runtime,
13354
+ configPath: settingsPath,
13355
+ // #4249: track unconditionally. Gating on `plan.hooksSurface ===
13356
+ // 'settings-json'` made tracking depend on an unasserted
13357
+ // installSurface/hooksSurface coupling — a descriptor that broke it would
13358
+ // silently drop this runtime out of validation. Everything recorded here
13359
+ // lands in settings.json by construction, and the registered-command
13360
+ // filter below already discards entries no hook actually references.
13361
+ configuredEntrypoints: settingsEntrypoints,
13362
+ };
12806
13363
  // #2979: local-install hook commands also use a runner GUI/minimal-PATH
12807
13364
  // runtimes can resolve. Bare `node` fails when the host launches the
12808
13365
  // runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
@@ -12816,19 +13373,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12816
13373
  // `node` command that recreates the #2979 failure.
12817
13374
  const localCmd = (hookFile) => localNodeRunner === null
12818
13375
  ? null
12819
- : projectShellCommandText({
13376
+ : hooksSurface.recordConfiguredHookCommand(projectShellCommandText({
12820
13377
  runnerToken: localNodeRunner,
12821
13378
  argTokens: [`${localPrefix}/hooks/${hookFile}`],
12822
13379
  runtime,
12823
13380
  platform: process.platform,
12824
- });
12825
- const localShellCmd = (hookFile) => buildLocalShellHookCommand({
13381
+ }), targetDir, hookFile, hookOpts);
13382
+ const localShellCmd = (hookFile) => hooksSurface.recordConfiguredHookCommand(buildLocalShellHookCommand({
12826
13383
  localPrefix,
12827
13384
  hookFile,
12828
13385
  bashRunner: localBashRunner,
12829
13386
  runtime,
12830
13387
  platform: process.platform,
12831
- });
13388
+ }), targetDir, hookFile, hookOpts);
12832
13389
  const statuslineCommand = isGlobal
12833
13390
  ? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
12834
13391
  : localCmd('gsd-statusline.js');
@@ -12898,6 +13455,31 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12898
13455
  ? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
12899
13456
  : localCmd('gsd-update-banner.js'));
12900
13457
 
13458
+ const registeredHookCommands = Object.values(settings.hooks || {})
13459
+ .flatMap(groups => Array.isArray(groups) ? groups : [])
13460
+ .flatMap(group => Array.isArray(group && group.hooks) ? group.hooks : [])
13461
+ .map(hook => hook && hook.command)
13462
+ .filter(command => typeof command === 'string');
13463
+ // #4249: match by the managed script's `/hooks/<basename>` path segment, not
13464
+ // by exact command-string equality. The blocking-guard hooks above register
13465
+ // only-if-absent, so a hook already present from a prior install keeps its
13466
+ // OLD command untouched — but `track()` always records the FRESHLY computed
13467
+ // command for it, which never equals what's actually persisted. Matching on
13468
+ // the segment (present in the persisted command either way, since every
13469
+ // entry.scriptPath is <configDir>/hooks/<name> by construction) keeps an
13470
+ // already-registered, still-active hook in the validated set instead of
13471
+ // silently dropping it (#4154 Blocker) — anchored on `/hooks/` rather than a
13472
+ // bare basename so an unrelated user command that merely mentions the same
13473
+ // filename can't false-positive into GSD's validated set.
13474
+ configuredEntrypoints.push(
13475
+ ...settingsEntrypoints.filter(entry => {
13476
+ const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
13477
+ return registeredHookCommands.some(command => command.includes(hooksSegment));
13478
+ }),
13479
+ );
13480
+ const statuslineEntrypoints = settingsEntrypoints.filter(entry => entry.command === statuslineCommand);
13481
+ const updateBannerEntrypoints = settingsEntrypoints.filter(entry => entry.command === updateBannerCommand);
13482
+
12901
13483
  // #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs.
12902
13484
  // Both fresh and upgrade paths apply only when worktrees are enabled for the project.
12903
13485
  // Never applies to global installs, non-Claude runtimes, or when the user already
@@ -12966,15 +13548,65 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12966
13548
  settings,
12967
13549
  statuslineCommand,
12968
13550
  updateBannerCommand,
13551
+ statuslineEntrypoints,
13552
+ updateBannerEntrypoints,
12969
13553
  runtime,
12970
13554
  configDir: targetDir,
12971
13555
  rollbackInstallerMigrations,
13556
+ configuredEntrypoints,
12972
13557
  };
12973
13558
  }
12974
13559
 
12975
- /**
12976
- * Apply statusline config, then print completion message
12977
- */
13560
+ // #4249 (review, Major): rollback consequence differs by runtime surface —
13561
+ // see docs/how-to/update-gsd.md's rollback-matrix paragraph, which this
13562
+ // mirrors. Codex reverts (pre-install snapshot restore); Cursor/Windsurf/
13563
+ // Kimi/Kimi Code/Cline already wrote their config file inside install(),
13564
+ // ahead of this gate, with no revert path, so it is left on disk broken;
13565
+ // every other (settings.json-based) runtime writes strictly after this gate,
13566
+ // so a failure here means nothing new was persisted for it.
13567
+ const ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES = new Set(['cursor', 'windsurf', 'kimi', 'kimi-code', 'cline']);
13568
+ function describeEntrypointConsequence(invalidRuntime) {
13569
+ if (invalidRuntime === 'codex') return 'reverted: its pre-install snapshot was restored';
13570
+ if (ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES.has(invalidRuntime)) return 'NOT reverted: its config file is already written and was left on disk — fix the reported path and rerun install';
13571
+ return 'not persisted: this runtime writes its config after this check';
13572
+ }
13573
+
13574
+ function assertConfiguredEntrypoints(entries) {
13575
+ // #4249: some writers push the same (configPath, scriptPath) pair more than
13576
+ // once (e.g. Kimi's context-monitor hook registered under several events,
13577
+ // or the portable resolver script shared by every portable JS hook) — keep
13578
+ // one so a broken entry is reported once, not once per duplicate.
13579
+ const seen = new Set();
13580
+ const deduped = (entries || []).filter((entry) => {
13581
+ const key = JSON.stringify([entry.configPath, entry.scriptPath]);
13582
+ if (seen.has(key)) return false;
13583
+ seen.add(key);
13584
+ return true;
13585
+ });
13586
+ const validation = hooksSurface.validateConfiguredEntrypoints(deduped);
13587
+ if (validation.ok) return;
13588
+
13589
+ const error = new Error(
13590
+ // #4249: lead each entry with its runtime, and name the actual consequence
13591
+ // for that runtime (review, Major) — the aggregate gate is all-or-nothing
13592
+ // across every runtime being installed, and a failure here can revert a
13593
+ // runtime whose own entrypoints were fine (see
13594
+ // rollbackFinalizedInstallerMigrations) while leaving another runtime's
13595
+ // already-written config broken on disk with no revert at all, so an
13596
+ // operator reading only this message must be able to tell WHOSE
13597
+ // entrypoint broke and WHAT that means for their config, not just that
13598
+ // something did.
13599
+ `Configured entrypoint validation failed: ${validation.invalid.map(({ runtime: invalidRuntime, role, path: invalidPath, reason }) => `${invalidRuntime} ${role} ${invalidPath} (${reason}) [${describeEntrypointConsequence(invalidRuntime)}]`).join(', ')}`,
13600
+ );
13601
+ error.configuredEntrypointValidation = validation;
13602
+ throw error;
13603
+ }
13604
+
13605
+ // #4249: `bannerOpts.configuredEntrypoints` is the ONLY source assertConfiguredEntrypoints
13606
+ // checks below — a caller that omits it (or calls finishInstall directly instead of
13607
+ // through installAllRuntimes) gets zero entrypoint validation, silently. installAllRuntimes
13608
+ // always passes the full set (per-runtime entries plus statusline/updateBanner); any other
13609
+ // caller must do the same for this gate to mean anything.
12978
13610
  function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
12979
13611
  // #2093: isKilo dropped — the Kilo permissions-writer call below is gated
12980
13612
  // on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
@@ -12988,6 +13620,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
12988
13620
  const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
12989
13621
  const plan = resolveInstallPlan(runtime);
12990
13622
 
13623
+ // #4249 Major: validate BEFORE this function's own settings.json write (and
13624
+ // before writeNonClaudeDefaults) instead of after. Cursor/Windsurf/Kimi/Cline
13625
+ // already persisted their config inside install() by this point, with no
13626
+ // rollback path covering those writes; Codex also persists inside install()
13627
+ // but its rollback binds to a full pre-install snapshot restore, so it IS
13628
+ // covered (see docs/how-to/update-gsd.md). For the settings-json surface
13629
+ // this ordering means a failing validation never reaches this function's
13630
+ // own write at all. On the production path this is a redundant backstop —
13631
+ // installAllRuntimes's own aggregate assertConfiguredEntrypoints call
13632
+ // already validates the superset before finishInstall runs for any
13633
+ // runtime — kept for a caller that invokes finishInstall directly.
13634
+ assertConfiguredEntrypoints(bannerOpts.configuredEntrypoints);
13635
+
12991
13636
  if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
12992
13637
  if (!isGlobal && !forceStatusline) {
12993
13638
  // Local installs skip statusLine by default: repo settings.json takes precedence over
@@ -13885,10 +14530,32 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
13885
14530
 
13886
14531
  const rollbackFinalizedInstallerMigrations = (error) => {
13887
14532
  const rollbackFailures = [];
14533
+ // #4249: this discriminates on the error's KIND, never on which runtime
14534
+ // owns the failing entrypoint. `wide` is true for ANY entrypoint-validation
14535
+ // failure from ANY runtime, by design: the aggregate gate exists so a
14536
+ // multi-runtime install cannot report success while one of its entrypoints
14537
+ // is broken, so an invalid Cline entrypoint reverts Codex's pre-install
14538
+ // snapshot too — even though Codex itself was fine and its own "Done!"
14539
+ // summary already printed. tests/configured-entrypoint-validation.test.cjs
14540
+ // ('an aggregate entrypoint validation failure rolls the Codex install
14541
+ // back') exercises exactly that, and it is the all-or-nothing behaviour
14542
+ // docs/how-to/update-gsd.md documents.
14543
+ //
14544
+ // What this narrows is the OTHER axis: a finalize-stage exception that is
14545
+ // not an entrypoint-validation failure at all — e.g. a sibling runtime's
14546
+ // permission-config write dying with EACCES — gets only the
14547
+ // installer-migrations-only rollback that Phase 4 specifies
14548
+ // (docs/installer-migrations.md#phase-4-installupdate-integration).
14549
+ // Un-installing (and, on update, downgrading) an already-"Done!" Codex over
14550
+ // an unrelated error is not an outcome any doc promises, while the sibling
14551
+ // surfaces that write config inside install() would keep theirs regardless.
14552
+ const wide = !!(error && error.configuredEntrypointValidation);
13888
14553
  for (const result of [...results].reverse()) {
13889
- if (!result || typeof result.rollbackInstallerMigrations !== 'function') continue;
14554
+ if (!result) continue;
14555
+ const rollback = (wide && result.rollbackPreInstallSnapshot) || result.rollbackInstallerMigrations;
14556
+ if (typeof rollback !== 'function') continue;
13890
14557
  try {
13891
- result.rollbackInstallerMigrations();
14558
+ rollback();
13892
14559
  } catch (rollbackError) {
13893
14560
  rollbackFailures.push({
13894
14561
  runtime: result.runtime,
@@ -13916,6 +14583,19 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
13916
14583
 
13917
14584
  const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
13918
14585
  try {
14586
+ const selectedConfiguredEntrypoints = (result) => {
14587
+ if (!result || result.skipped) return [];
14588
+ const useStatusline = statuslineRuntimes.includes(result.runtime)
14589
+ && shouldInstallStatusline
14590
+ && (isGlobal || forceStatusline);
14591
+ return [
14592
+ ...(result.configuredEntrypoints || []),
14593
+ ...(useStatusline ? (result.statuslineEntrypoints || []) : []),
14594
+ ...(shouldInstallBanner ? (result.updateBannerEntrypoints || []) : []),
14595
+ ];
14596
+ };
14597
+ assertConfiguredEntrypoints(results.flatMap(selectedConfiguredEntrypoints));
14598
+
13919
14599
  const printSummaries = () => {
13920
14600
  for (const result of results) {
13921
14601
  if (result && result.skipped) continue;
@@ -13929,7 +14609,11 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
13929
14609
  result.runtime,
13930
14610
  isGlobal,
13931
14611
  result.configDir,
13932
- { shouldInstallBanner: !!shouldInstallBanner, bannerCommand: result.updateBannerCommand }
14612
+ {
14613
+ shouldInstallBanner: !!shouldInstallBanner,
14614
+ bannerCommand: result.updateBannerCommand,
14615
+ configuredEntrypoints: selectedConfiguredEntrypoints(result),
14616
+ }
13933
14617
  );
13934
14618
  }
13935
14619
  };
@@ -14117,6 +14801,7 @@ module.exports = {
14117
14801
  reportLocalPatches,
14118
14802
  validateHookFields,
14119
14803
  populatePristineDir,
14804
+ describeBaselineCoverage,
14120
14805
  _resolveUserArtifactStagingRoot,
14121
14806
  _tryResolveUserArtifactStagingRoot,
14122
14807
  finishInstall,