@opengsd/gsd-core 1.14.0 → 1.16.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 (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
package/bin/install.js CHANGED
@@ -49,7 +49,7 @@ const { isTestHomeGuardRefusal } = require('../gsd-core/bin/lib/real-home-guard.
49
49
  // (getConfigDirFromHome and the runtime-content-rewrite loops below) — #2876
50
50
  // retired the re-export; tests now import getDirName directly from
51
51
  // gsd-core/bin/lib/runtime-name-policy.cjs.
52
- const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
52
+ const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, hostBehaviorsFor } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
53
53
  const {
54
54
  applyWorktreeBaseRef,
55
55
  readBaseRefFromSettings,
@@ -104,7 +104,7 @@ const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
104
104
  */
105
105
  function shouldNormalizeHyphenNamespaceInAgentBody(runtime) {
106
106
  if (typeof runtime !== 'string' || runtime === '') return false;
107
- return _hostBehaviors(runtime).hyphenNameAgentBody === true;
107
+ return hostBehaviorsFor(runtime).hyphenNameAgentBody === true;
108
108
  }
109
109
 
110
110
  /**
@@ -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', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs', 'macos-conformance-tier.generated.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', 'vendor-bundle.cjs', 'registration-ledger-preload.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.
@@ -462,7 +473,7 @@ const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-s
462
473
  * @returns {string}
463
474
  */
464
475
  function resolveSharedHooksDirName(runtime) {
465
- const raw = _hostBehaviors(runtime).sharedHooksDirName;
476
+ const raw = hostBehaviorsFor(runtime).sharedHooksDirName;
466
477
  if (typeof raw !== 'string') return SHARED_HOOKS_DIR_DEFAULT;
467
478
  const name = raw.trim();
468
479
  if (name === '') return SHARED_HOOKS_DIR_DEFAULT;
@@ -617,68 +628,6 @@ try {
617
628
  _installedCapabilityRegistry = _capabilityRegistry;
618
629
  }
619
630
 
620
- // Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
621
- // ONLY when the first-party capability registry cannot be loaded (a broken bundle).
622
- // Without it, a registry-load failure would make `_hostBehaviors('claude')` return
623
- // {} and silently route a claude LOCAL install to the repo-shared, committed
624
- // `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
625
- // engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
626
- // hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
627
- // The live descriptor (capabilities/claude/capability.json) remains the source of
628
- // truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
629
- //
630
- // #2870: NOT routed through the Install Scope Module (resolveScope,
631
- // src/install-scope.cts) despite that module owning per-scope settings-file
632
- // resolution elsewhere in this file. resolveScope's own descriptor lookup
633
- // goes through the SAME capability registry require this floor exists to
634
- // survive the failure of (see getRegistry() in install-scope.cts) — so on
635
- // exactly the "registry failed to load" path this constant is for,
636
- // resolveScope would throw too. Routing through it here would trade a
637
- // graceful, documented degrade for a crash in the one case this floor was
638
- // added to prevent. This hardcoded literal is the correct, honest answer,
639
- // not an un-migrated leftover.
640
- const FALLBACK_HOST_BEHAVIORS = Object.freeze({
641
- claude: Object.freeze({
642
- settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
643
- permissionsSchema: 'claude',
644
- sourceMarkerFile: '.gsd-source',
645
- hyphenNameAgentBody: true,
646
- legacyCommandsGsdInstallMigration: true,
647
- legacyCommandsGsdUninstall: 'global',
648
- }),
649
- // antigravity's global config dir is resolved dynamically (env-overridable,
650
- // multi-segment) via resolveAntigravityGlobalDir in getConfigDirFromHome. If the
651
- // registry fails to load, this floor keeps that routing intact instead of
652
- // silently falling through to the generic getGlobalConfigHomeFragment default
653
- // (which would return the wrong '.claude' fragment). (ADR-1239 / #2096)
654
- antigravity: Object.freeze({ globalDirResolver: 'antigravity' }),
655
- });
656
-
657
- /**
658
- * Resolve a runtime's host behaviors from a capability registry, with the
659
- * #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
660
- * is unavailable. Registry is passed in so this is unit-testable under a
661
- * simulated registry-load failure. (ADR-1239 / #2086)
662
- */
663
- function _resolveHostBehaviors(runtime, registry) {
664
- const cap = registry && registry.runtimes && registry.runtimes[runtime];
665
- const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
666
- if (declared) return declared;
667
- return FALLBACK_HOST_BEHAVIORS[runtime] || {};
668
- }
669
-
670
- /**
671
- * Host-specific install behaviors, declared on the runtime descriptor
672
- * (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
673
- * scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
674
- * for runtimes that declare none, so every behavior branch degrades to the
675
- * generic path by default — EXCEPT the reference host's #338-critical keys, which
676
- * fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
677
- */
678
- function _hostBehaviors(runtime) {
679
- return _resolveHostBehaviors(runtime, _capabilityRegistry);
680
- }
681
-
682
631
  /**
683
632
  * Read a runtime's documentation-sourced `hostIntegration.dispatch` axes
684
633
  * (ADR-1239 Phase A — `capabilities/<runtime>/capability.json`
@@ -686,7 +635,7 @@ function _hostBehaviors(runtime) {
686
635
  * background, backgroundDispatch, subagentToolkit}`. These are validated,
687
636
  * closed-vocabulary FACTS about what the runtime's real dispatch primitive
688
637
  * supports (never inferred) — see `docs/reference/host-integration-capability-
689
- * matrix.md` for citations. Unlike `_hostBehaviors` (install *policy*), this is
638
+ * matrix.md` for citations. Unlike `hostBehaviorsFor` (install *policy*), this is
690
639
  * the negotiated *capability* surface; #2284 is its first content-projection
691
640
  * consumer (previously read only by `shouldFlattenDispatch`). Returns `{}` if
692
641
  * the registry or the runtime's descriptor is unavailable, so callers must
@@ -812,6 +761,7 @@ const {
812
761
  applyInstallerMigrationPlan,
813
762
  discoverInstallerMigrations,
814
763
  MANIFEST_SCHEMA_VERSION,
764
+ readInstallManifest,
815
765
  runInstallerMigrations,
816
766
  } = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
817
767
  const {
@@ -821,6 +771,7 @@ const {
821
771
  } = require(path.join(_gsdLibDir, 'installer-migration-report.cjs'));
822
772
  const {
823
773
  resolveRuntimeArtifactLayout,
774
+ resolveAdvertisedNewProject,
824
775
  } = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
825
776
  const {
826
777
  readSurface,
@@ -925,6 +876,25 @@ const hasSkillsRoot = args.includes('--skills-root');
925
876
  const hasPortableHooks = args.includes('--portable-hooks') || process.env.GSD_PORTABLE_HOOKS === '1';
926
877
  const hasMinimal = args.includes('--minimal') || args.includes('--core-only');
927
878
  const hasDryRun = args.includes('--dry-run');
879
+ // #4377: emit project-relative `@` includes (`.claude/gsd-core/...`) for a
880
+ // LOCAL install instead of this checkout's absolute path.
881
+ //
882
+ // Opt-in, and it stays opt-in: absolute includes work for a single checkout,
883
+ // which is nearly everyone, and flipping the default would change every
884
+ // existing local install to solve a problem those users do not have. The
885
+ // people who need it know they do — they run the same repo from several git
886
+ // worktrees, where a baked absolute path means every worktree reads its
887
+ // workflow prose out of whichever checkout happened to run the installer, and
888
+ // updating that one checkout breaks all the others at once with no way to
889
+ // stage it.
890
+ //
891
+ // Exported through the environment rather than threaded as a parameter,
892
+ // exactly like --portable-hooks/GSD_PORTABLE_HOOKS above: five separate seams
893
+ // compute a path prefix (the install engine, both rewrite entry points, the
894
+ // install plan, and applySurface), and one variable they all read cannot fall
895
+ // out of sync the way five signatures can.
896
+ const hasRelativeIncludes = args.includes('--relative-includes') || process.env.GSD_RELATIVE_INCLUDES === '1';
897
+ if (hasRelativeIncludes) process.env.GSD_RELATIVE_INCLUDES = '1';
928
898
  // #3031: opt-in reclaim of the GSD artifacts a PRE-#2755 `--kimi-code` install
929
899
  // orphaned in Kimi CLI's `~/.kimi`. Opt-in and not automatic because the stale
930
900
  // block is BYTE-IDENTICAL to a legitimate Kimi CLI one — both runtimes render
@@ -1147,13 +1117,13 @@ function getConfigDirFromHome(runtime, isGlobal) {
1147
1117
  // !isGlobal returns at the top of this function.)
1148
1118
  // Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
1149
1119
  // `runtime === 'antigravity'` literal into a read of the runtime's
1150
- // `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which
1151
- // also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is
1120
+ // `hostBehaviors.globalDirResolver` descriptor field (via hostBehaviorsFor, which
1121
+ // also degrades to its #338 floor on registry-load failure). This is
1152
1122
  // antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which
1153
1123
  // windsurf also declares — see capabilities/windsurf/capability.json — and
1154
1124
  // would wrongly route windsurf's global dir through
1155
1125
  // resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity.
1156
- if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') {
1126
+ if (hostBehaviorsFor(runtime).globalDirResolver === 'antigravity') {
1157
1127
  const antigravityDir = resolveAntigravityGlobalDir();
1158
1128
  const rel = path.relative(os.homedir(), antigravityDir);
1159
1129
  const segments = rel.split(path.sep).filter(Boolean);
@@ -1252,7 +1222,7 @@ if (hasUninstall) {
1252
1222
 
1253
1223
  // Show help if requested
1254
1224
  if (hasHelp) {
1255
- 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`);
1225
+ 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`);
1256
1226
  process.exit(0);
1257
1227
  }
1258
1228
 
@@ -1563,12 +1533,12 @@ function getCommitAttribution(runtime) {
1563
1533
 
1564
1534
  let result;
1565
1535
 
1566
- const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver;
1536
+ const _attrResolverKey = hostBehaviorsFor(runtime).attributionConfigResolver;
1567
1537
  if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) {
1568
1538
  const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey];
1569
1539
  const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
1570
1540
  result = (config && config.disable_ai_attribution === true) ? null : undefined;
1571
- } else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
1541
+ } else if (hostBehaviorsFor(runtime).attributionSource === 'settings-json-commit') {
1572
1542
  // Claude Code
1573
1543
  const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
1574
1544
  if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
@@ -1625,19 +1595,20 @@ const claudeToOpencodeTools = {
1625
1595
  WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
1626
1596
  };
1627
1597
 
1628
- // Tool name mapping from Claude Code to Gemini CLI
1629
- // Gemini CLI uses snake_case built-in tool names
1630
- const claudeToGeminiTools = {
1631
- Read: 'read_file',
1598
+ // Tool name mapping from Claude Code to Antigravity
1599
+ // Antigravity uses Gemini's snake_case built-in tool names
1600
+ const claudeToAntigravityTools = {
1601
+ // #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
1602
+ Read: 'view_file',
1632
1603
  Write: 'write_file',
1633
- Edit: 'replace',
1634
- Bash: 'run_shell_command',
1604
+ Edit: 'replace_file_content',
1605
+ Bash: 'run_command',
1635
1606
  Glob: 'glob',
1636
- Grep: 'search_file_content',
1607
+ Grep: 'grep_search',
1637
1608
  WebSearch: 'google_web_search',
1638
1609
  WebFetch: 'web_fetch',
1639
1610
  TodoWrite: 'write_todos',
1640
- };
1611
+ }
1641
1612
 
1642
1613
  // Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
1643
1614
  // Kimi custom agent YAML requires fully-qualified module paths.
@@ -1687,24 +1658,24 @@ function convertToolName(claudeTool) {
1687
1658
  }
1688
1659
 
1689
1660
  /**
1690
- * Convert a Claude Code tool name to Gemini CLI format
1691
- * - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.)
1692
- * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini
1693
- * - Filters out Task/Agent — agents are auto-registered as tools in Gemini
1694
- * @returns {string|null} Gemini tool name, or null if tool should be excluded
1661
+ * Convert a Claude Code tool name to Antigravity format
1662
+ * - Applies Claude→Antigravity mapping (Read→read_file, Bash→run_shell_command, etc.)
1663
+ * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Antigravity
1664
+ * - Filters out Task/Agent — agents are auto-registered as tools in Antigravity
1665
+ * @returns {string|null} Antigravity tool name, or null if tool should be excluded
1695
1666
  */
1696
- function convertGeminiToolName(claudeTool) {
1667
+ function convertAntigravityToolName(claudeTool) {
1697
1668
  // MCP tools: exclude — auto-discovered from mcpServers config at runtime
1698
1669
  if (claudeTool.startsWith('mcp__')) {
1699
1670
  return null;
1700
1671
  }
1701
1672
  // Task/Agent: exclude — agents are auto-registered as callable tools.
1702
- // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool;
1703
- // emitting it causes frontmatter validation errors (#3362).
1704
- // Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool;
1705
- // the lowercase fallback would emit an invalid 'skill'/'slashcommand' name
1706
- // that fails frontmatter validation (tools.N: Invalid tool name) and aborts
1707
- // the entire agent load (#1394).
1673
+ // AskUserQuestion: exclude — Antigravity (Gemini tool dialect) does not expose
1674
+ // an ask_user tool; emitting it causes frontmatter validation errors (#3362).
1675
+ // Skill/SlashCommand: exclude — Antigravity (Gemini tool dialect) has no 'skill'
1676
+ // built-in tool; the lowercase fallback would emit an invalid
1677
+ // 'skill'/'slashcommand' name that fails frontmatter validation
1678
+ // (tools.N: Invalid tool name) and aborts the entire agent load (#1394).
1708
1679
  if (
1709
1680
  claudeTool === 'Task' ||
1710
1681
  claudeTool === 'Agent' ||
@@ -1716,8 +1687,8 @@ function convertGeminiToolName(claudeTool) {
1716
1687
  return null;
1717
1688
  }
1718
1689
  // Check for explicit mapping
1719
- if (claudeToGeminiTools[claudeTool]) {
1720
- return claudeToGeminiTools[claudeTool];
1690
+ if (claudeToAntigravityTools[claudeTool]) {
1691
+ return claudeToAntigravityTools[claudeTool];
1721
1692
  }
1722
1693
  // Default: lowercase
1723
1694
  return claudeTool.toLowerCase();
@@ -2023,7 +1994,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
2023
1994
  const names = cmdNames || readGsdCommandNames();
2024
1995
  const normalizedBody = transformContentToHyphen(body, names);
2025
1996
 
2026
- const description = extractFrontmatterField(frontmatter, 'description') || '';
1997
+ // #4324: the description is the text the host's skill picker renders, so it
1998
+ // needs the same hyphen normalisation the body gets — otherwise a `/gsd:<cmd>`
1999
+ // mention in a command description ships the retired colon form to the user.
2000
+ const description = transformContentToHyphen(
2001
+ extractFrontmatterField(frontmatter, 'description') || '', names,
2002
+ );
2027
2003
  const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
2028
2004
  const agent = extractFrontmatterField(frontmatter, 'agent');
2029
2005
  // #769: preserve context: from source command files so it is emitted into
@@ -2047,13 +2023,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
2047
2023
  // Hermes' SKILL.md spec lists `version` as a required frontmatter field.
2048
2024
  // Track GSD's package version so Hermes' skill_view() reports a stable
2049
2025
  // identifier per install.
2050
- if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
2026
+ if (hostBehaviorsFor(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
2051
2027
  // #778 (b) — numeric priority for /skills ordering, declared on the runtime
2052
2028
  // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
2053
2029
  // runtimes that declare the flag so Claude/Hermes skill frontmatter is
2054
2030
  // unchanged (they ignore the field, but we keep their output byte-stable).
2055
2031
  // skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2086)
2056
- if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
2032
+ if (hostBehaviorsFor(runtime).skillPriorityFrontmatter) {
2057
2033
  const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
2058
2034
  ? skillName.slice(4)
2059
2035
  : skillName;
@@ -2486,12 +2462,16 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
2486
2462
  const color = extractFrontmatterField(frontmatter, 'color');
2487
2463
  const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2488
2464
 
2489
- // Map tools to Gemini equivalents (reuse existing convertGeminiToolName)
2465
+ // Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName)
2490
2466
  const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2491
- const mappedTools = claudeTools.map(t => convertGeminiToolName(t)).filter(Boolean);
2467
+ const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
2492
2468
 
2493
2469
  // #2876: quote description for the same reason as the skill variant.
2494
- let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`;
2470
+ // #4705: tools is a YAML SEQUENCE of native names (see the src twin).
2471
+ const toolsBlock = mappedTools.length > 0
2472
+ ? `tools:\n${mappedTools.map((t) => `- ${t}`).join('\n')}\n`
2473
+ : 'tools: []\n';
2474
+ let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n${toolsBlock}`;
2495
2475
  if (color) fm += `color: ${color}\n`;
2496
2476
  fm += '---';
2497
2477
 
@@ -2514,20 +2494,11 @@ function yamlIdentifier(value) {
2514
2494
  return yamlQuote(text);
2515
2495
  }
2516
2496
 
2497
+ // The frontmatter block is the one the one fence owner (`locateFrontmatterFence`) finds, read
2498
+ // through the conversion module's reader — never a local `indexOf('---', 3)` scan, which ended
2499
+ // the block at the first `---` anywhere, even inside a value (found while implementing #5105).
2517
2500
  function extractFrontmatterAndBody(content) {
2518
- if (!content.startsWith('---')) {
2519
- return { frontmatter: null, body: content };
2520
- }
2521
-
2522
- const endIndex = content.indexOf('---', 3);
2523
- if (endIndex === -1) {
2524
- return { frontmatter: null, body: content };
2525
- }
2526
-
2527
- return {
2528
- frontmatter: content.substring(3, endIndex).trim(),
2529
- body: content.substring(endIndex + 3),
2530
- };
2501
+ return runtimeArtifactConversion.extractFrontmatterAndBody(content);
2531
2502
  }
2532
2503
 
2533
2504
  function extractFrontmatterField(frontmatter, fieldName) {
@@ -2772,7 +2743,7 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
2772
2743
  // is not formally documented (thin SPA docs) — descriptor-driven, single
2773
2744
  // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
2774
2745
  // best-effort.
2775
- const soloStage = _hostBehaviors('trae').soloStageMetadata;
2746
+ const soloStage = hostBehaviorsFor('trae').soloStageMetadata;
2776
2747
  if (soloStage) fm += `stage: ${soloStage}\n`;
2777
2748
  fm += '---';
2778
2749
  return `${fm}\n${body}`;
@@ -2947,6 +2918,8 @@ function convertClaudeCommandToClineSkill(content, skillName, runtime = null, cm
2947
2918
  let description = extractFrontmatterField(frontmatter, 'description');
2948
2919
  if (!description) description = `Run GSD workflow ${skillName}.`;
2949
2920
  description = toSingleLine(description);
2921
+ // #4324: same reason as the Claude skill converter above.
2922
+ description = transformContentToHyphen(description, names);
2950
2923
  // Cline documented max is 1024 code points (not UTF-16 code units).
2951
2924
  // Use Array.from to iterate by code point so that multibyte characters
2952
2925
  // (e.g. emoji, astral-plane chars) are never split, which would produce
@@ -3763,7 +3736,7 @@ const HERMES_DISPATCH_TOOL_CONFIG = Object.freeze({
3763
3736
  */
3764
3737
  function convertClaudeToHermesMarkdown(content, ctx) {
3765
3738
  const runtime = (ctx && ctx.runtime) || 'hermes';
3766
- const b = _hostBehaviors(runtime).brandingRewrites;
3739
+ const b = hostBehaviorsFor(runtime).brandingRewrites;
3767
3740
  let converted = content;
3768
3741
  if (b) {
3769
3742
  converted = converted.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
@@ -3921,6 +3894,22 @@ Typed mapping (agent_type-capable schema only):
3921
3894
  never fabricate a manual worktree protocol — route through the negotiated
3922
3895
  isolation adapter, which still fails closed for hosts declaring \`none\` (#3360).
3923
3896
 
3897
+ Foreground handoffs:
3898
+ - spawn_agent is asynchronous. When the source Agent(...) or Task(...) declares
3899
+ run_in_background=false, call collaboration.wait_agent(timeout_ms=...) immediately after
3900
+ spawn and keep the parent turn active until that child returns a terminal result.
3901
+ - collaboration.wait_agent is a mailbox wakeup, NOT a completion oracle: "Wait completed"
3902
+ can mean only that a child sent an interim MESSAGE or status update. After every wakeup,
3903
+ inspect the named child's update/status. Only a FINAL_ANSWER or a terminal agent status
3904
+ (completed, failed, or cancelled) ends the foreground handoff.
3905
+ - On an interim MESSAGE or any non-terminal status, do not report an outcome, send a
3906
+ continuation, start parent work, or end the parent turn. Call collaboration.wait_agent
3907
+ again for the same child. If a terminal response is absent after an abnormal end, reconcile
3908
+ the workflow's durable artifacts before classifying the child.
3909
+ - This applies to one foreground child as well as fan-out. The child retains its workflow's
3910
+ own checkpoint loop; do not report an outcome or start any further parent work before its
3911
+ terminal result is available.
3912
+
3924
3913
  Generic-agent workaround (multi_agent_v1 schema — NO agent_type field):
3925
3914
  When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch
3926
3915
  (\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation
@@ -3948,6 +3937,9 @@ Spawn restriction:
3948
3937
  defaulting to inline execution.
3949
3938
 
3950
3939
  Parallel fan-out:
3940
+ - For each child, loop on collaboration.wait_agent(timeout_ms=...) until its own terminal
3941
+ result is observed. A mailbox update from one child never completes another child, and an
3942
+ interim MESSAGE never completes its sender.
3951
3943
  - Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
3952
3944
  - Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
3953
3945
 
@@ -6978,7 +6970,7 @@ function writeCopilotHookConfig(targetDir) {
6978
6970
  * model aliases). Preserves an explicit `true` opt-in and existing values.
6979
6971
  */
6980
6972
  function writeNonClaudeDefaults(runtime) {
6981
- if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
6973
+ if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
6982
6974
  const gsdDir = path.join(os.homedir(), '.gsd');
6983
6975
  const defaultsPath = path.join(gsdDir, 'defaults.json');
6984
6976
  let releaseLock = null;
@@ -7209,20 +7201,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7209
7201
  // Runtime-neutral agent name replacement (#766)
7210
7202
  convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
7211
7203
 
7212
- // Check if content has frontmatter
7213
- if (!convertedContent.startsWith('---')) {
7214
- return convertedContent;
7215
- }
7216
-
7217
- // Find the end of frontmatter
7218
- const endIndex = convertedContent.indexOf('---', 3);
7219
- if (endIndex === -1) {
7204
+ // The frontmatter block, as the one fence owner finds it (none → nothing to convert).
7205
+ const { frontmatter, body } = extractFrontmatterAndBody(convertedContent);
7206
+ if (frontmatter === null) {
7220
7207
  return convertedContent;
7221
7208
  }
7222
7209
 
7223
- const frontmatter = convertedContent.substring(3, endIndex).trim();
7224
- const body = convertedContent.substring(endIndex + 3);
7225
-
7226
7210
  // Parse frontmatter line by line (simple YAML parsing)
7227
7211
  const lines = frontmatter.split('\n');
7228
7212
  const newLines = [];
@@ -7376,20 +7360,12 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
7376
7360
  // Runtime-neutral agent name replacement (#766)
7377
7361
  convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
7378
7362
 
7379
- // Check if content has frontmatter
7380
- if (!convertedContent.startsWith('---')) {
7363
+ // The frontmatter block, as the one fence owner finds it (none → nothing to convert).
7364
+ const { frontmatter, body } = extractFrontmatterAndBody(convertedContent);
7365
+ if (frontmatter === null) {
7381
7366
  return convertedContent;
7382
7367
  }
7383
7368
 
7384
- // Find the end of frontmatter
7385
- const endIndex = convertedContent.indexOf('---', 3);
7386
- if (endIndex === -1) {
7387
- return convertedContent;
7388
- }
7389
-
7390
- const frontmatter = convertedContent.substring(3, endIndex).trim();
7391
- const body = convertedContent.substring(endIndex + 3);
7392
-
7393
7369
  // Parse frontmatter line by line (simple YAML parsing)
7394
7370
  const lines = frontmatter.split('\n');
7395
7371
  const newLines = [];
@@ -7763,7 +7739,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7763
7739
  },
7764
7740
  },
7765
7741
  // qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via
7766
- // _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering
7742
+ // hostBehaviorsFor(ctx.runtime).brandingRewrites — EXACT regexes/ordering
7767
7743
  // preserved from the prior hardcoded-literal versions (including the
7768
7744
  // qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose
7769
7745
  // target is derived as `${b['.claude/']}skills/`).
@@ -7771,7 +7747,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7771
7747
  md: (content, ctx) => {
7772
7748
  // Guarded (post-review #2092): degrade closed to a no-op if the
7773
7749
  // registry fails to load, instead of throwing on `b['CLAUDE.md']`.
7774
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7750
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7775
7751
  if (b) {
7776
7752
  content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
7777
7753
  // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
@@ -7781,7 +7757,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7781
7757
  return content;
7782
7758
  },
7783
7759
  js: (content, ctx) => {
7784
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7760
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7785
7761
  if (b) {
7786
7762
  content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
7787
7763
  content = content.replace(/\.claude\//g, b['.claude/']);
@@ -7800,7 +7776,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7800
7776
  // hostIntegration.dispatch facts.
7801
7777
  md: (content, ctx) => convertClaudeToHermesMarkdown(content, ctx),
7802
7778
  js: (content, ctx) => {
7803
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7779
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7804
7780
  if (b) {
7805
7781
  content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
7806
7782
  content = content.replace(/\.claude\//g, b['.claude/']);
@@ -7914,44 +7890,63 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7914
7890
  content = filterRuntimeNotesForTarget(content, runtime);
7915
7891
 
7916
7892
  if (!dispatch.mdSkipGenericRewrite) {
7917
- const globalClaudeRegex = /~\/\.claude\//g;
7918
- const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
7919
- const localClaudeRegex = /\.\/\.claude\//g;
7920
- content = content.replace(globalClaudeRegex, pathPrefix);
7921
- content = content.replace(globalClaudeHomeRegex, pathPrefix);
7922
- content = content.replace(localClaudeRegex, `./${dirName}/`);
7923
- // #3544 review (Finding 1 fallout): guarded with the SAME
7924
- // negative-lookahead convention already used at ~:2859-2860 below
7925
- // ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
7926
- // is satisfied by ANY non-word character, including '-' — so for a
7927
- // --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
7928
- // pathPrefix '$HOME/.claude-work/'), this pass re-matched the
7929
- // '$HOME/.claude' PREFIX of its own slash-form output (lines above)
7930
- // and re-appended the full prefix, corrupting every emitted path to
7931
- // '$HOME/.claude-work-work/...'. Harmless no-op for the literal
7932
- // default '.claude' (self-replace with an identical string), which
7933
- // is why this went undetected until a non-default config-dir name
7934
- // was exercised.
7935
- content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7936
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7937
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7938
- content = content.replace(/~\/\.qwen\//g, pathPrefix);
7939
- content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
7940
- content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
7941
- content = content.replace(/~\/\.hermes\//g, pathPrefix);
7942
- content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
7943
- content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
7944
- // #3544: restore @-file-reference lines to the tilde form Claude Code
7945
- // actually expands — the SAME correction #3133 already applies to
7946
- // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
7947
- // restoreClaudeGlobalAtRefTilde's doc comment in
7948
- // runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
7949
- // emit path, which never had it: every @~/.claude/gsd-core/… include
7950
- // in a global install's workflows/references tree silently resolved
7951
- // to nothing (54 includes across 22 files on a live install).
7952
- if (runtime === 'claude') {
7953
- content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
7954
- }
7893
+ // #4377: with a project-relative prefix, mask `${VAR:-default}` shell
7894
+ // defaults out of the substitutions below and restore them after. The
7895
+ // runtime launcher snippet probes gsd-tools through a chain of those
7896
+ // (`${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/...`, one per
7897
+ // runtime); they are shell word expansions, not markdown @ includes,
7898
+ // and a relative value there resolves against the shell's cwd instead
7899
+ // of the project. Swapping an include that points at the wrong
7900
+ // checkout for a path that points at nothing is not a fix, and the
7901
+ // launcher already probes `$(git rev-parse --show-toplevel)/.claude`
7902
+ // first, so the multi-worktree case is handled before these defaults
7903
+ // are ever reached. The shared helper is the single owner of the
7904
+ // balanced masking grammar used by this path and the rewrite engine.
7905
+ const rewriteGenericPaths = (body) => {
7906
+ content = body;
7907
+ const globalClaudeRegex = /~\/\.claude\//g;
7908
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
7909
+ const localClaudeRegex = /\.\/\.claude\//g;
7910
+ content = content.replace(globalClaudeRegex, pathPrefix);
7911
+ content = content.replace(globalClaudeHomeRegex, pathPrefix);
7912
+ content = content.replace(localClaudeRegex, `./${dirName}/`);
7913
+ // #3544 review (Finding 1 fallout): guarded with the SAME
7914
+ // negative-lookahead convention already used at ~:2859-2860 below
7915
+ // ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
7916
+ // is satisfied by ANY non-word character, including '-' — so for a
7917
+ // --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
7918
+ // pathPrefix '$HOME/.claude-work/'), this pass re-matched the
7919
+ // '$HOME/.claude' PREFIX of its own slash-form output (lines above)
7920
+ // and re-appended the full prefix, corrupting every emitted path to
7921
+ // '$HOME/.claude-work-work/...'. Harmless no-op for the literal
7922
+ // default '.claude' (self-replace with an identical string), which
7923
+ // is why this went undetected until a non-default config-dir name
7924
+ // was exercised.
7925
+ content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7926
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
7927
+ content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
7928
+ content = content.replace(/~\/\.qwen\//g, pathPrefix);
7929
+ content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
7930
+ content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
7931
+ content = content.replace(/~\/\.hermes\//g, pathPrefix);
7932
+ content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
7933
+ content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
7934
+ // #3544: restore @-file-reference lines to the tilde form Claude Code
7935
+ // actually expands — the SAME correction #3133 already applies to
7936
+ // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
7937
+ // restoreClaudeGlobalAtRefTilde's doc comment in
7938
+ // runtime-artifact-conversion.cts). This is the gsd-core/ spec-tree
7939
+ // emit path, which never had it: every @~/.claude/gsd-core/… include
7940
+ // in a global install's workflows/references tree silently resolved
7941
+ // to nothing (54 includes across 22 files on a live install).
7942
+ if (hostBehaviorsFor(runtime).restoreAtRefTildeInSpecTree) {
7943
+ content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
7944
+ }
7945
+ return content;
7946
+ };
7947
+ content = runtimeArtifactConversion._isRelativePathPrefix(pathPrefix)
7948
+ ? runtimeArtifactConversion._withShellDefaultsPreserved(content, rewriteGenericPaths)
7949
+ : rewriteGenericPaths(content);
7955
7950
  }
7956
7951
  content = processAttribution(content, getCommitAttribution(runtime));
7957
7952
 
@@ -7960,7 +7955,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7960
7955
  // copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
7961
7956
  // _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
7962
7957
  // live in real installs (it is a no-op for files without those lines).
7963
- if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
7958
+ if (!hostBehaviorsFor(runtime).authorsCanonicalWorkflow) {
7964
7959
  content = _stampNonClaudeRuntimeDefaults(content, runtime);
7965
7960
  }
7966
7961
 
@@ -8310,7 +8305,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8310
8305
  // `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot.
8311
8306
  const targetDir = isGlobal
8312
8307
  ? getGlobalConfigDir(runtime, explicitConfigDir)
8313
- : _hostBehaviors(runtime).localTargetIsProjectRoot
8308
+ : hostBehaviorsFor(runtime).localTargetIsProjectRoot
8314
8309
  ? process.cwd()
8315
8310
  : path.join(process.cwd(), dirName);
8316
8311
 
@@ -8418,7 +8413,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8418
8413
  }
8419
8414
 
8420
8415
  // 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
8421
- if (_hostBehaviors(runtime).tomlConfigInstall) {
8416
+ if (hostBehaviorsFor(runtime).tomlConfigInstall) {
8422
8417
  const codexAgentsDir = path.join(targetDir, 'agents');
8423
8418
  if (fs.existsSync(codexAgentsDir)) {
8424
8419
  const tomlFiles = fs.readdirSync(codexAgentsDir);
@@ -8532,7 +8527,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8532
8527
  // global cross-tool ~/.agents/AGENTS.md target.
8533
8528
  // Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'`
8534
8529
  // into hostBehaviors.clineRulesSurface.
8535
- if (_hostBehaviors(runtime).clineRulesSurface) {
8530
+ if (hostBehaviorsFor(runtime).clineRulesSurface) {
8536
8531
  const clinerulesDir = path.join(targetDir, '.clinerules');
8537
8532
  for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) {
8538
8533
  const p = path.join(clinerulesDir, rel);
@@ -8582,7 +8577,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8582
8577
  // GSD-managed hook entries from hooks.json and clean up the managed hook
8583
8578
  // scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a
8584
8579
  // hardcoded `isCursor` branch.
8585
- if (_hostBehaviors(runtime).hooksJsonSurface) {
8580
+ if (hostBehaviorsFor(runtime).hooksJsonSurface) {
8586
8581
  const hooksJsonCleanup = removeCursorHooksJson(targetDir);
8587
8582
  if (hooksJsonCleanup.changed) {
8588
8583
  removedCount++;
@@ -8642,7 +8637,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8642
8637
 
8643
8638
  // 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
8644
8639
  // #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
8645
- if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
8640
+ if (!isGlobal && hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat') {
8646
8641
  const commandsDir = path.join(targetDir, 'commands');
8647
8642
  // Remove flat gsd-*.md files (current layout after #1367 fix)
8648
8643
  if (fs.existsSync(commandsDir)) {
@@ -8708,7 +8703,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8708
8703
  // removes the directory; we must preserve/restore user artifacts before that path.
8709
8704
  // This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
8710
8705
  // was already removed and skip if so (idempotent).
8711
- if (_hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
8706
+ if (hostBehaviorsFor(runtime).legacyCommandsGsdCleanup === true) {
8712
8707
  // dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
8713
8708
  // that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
8714
8709
  // attempt migration. In practice _runLegacyUninstallCleanup removes it first,
@@ -8923,7 +8918,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8923
8918
  // that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only
8924
8919
  // GSD's own plugin file is removed; the plugins/ dir is pruned only if it
8925
8920
  // becomes empty, preserving any user-authored plugins for that host.
8926
- const _np = _hostBehaviors(runtime).nativePlugin;
8921
+ const _np = hostBehaviorsFor(runtime).nativePlugin;
8927
8922
  if (_np) {
8928
8923
  const pluginsDir = path.join(targetDir, _np.dir);
8929
8924
  const pluginPath = path.join(pluginsDir, _np.file);
@@ -9088,7 +9083,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9088
9083
  // to preserve any user-added allow/deny entries.
9089
9084
  // Uses a local flag to avoid the shared `settingsModified` producing a false
9090
9085
  // "Removed GSD permissions" message when only hooks/statusline changed.
9091
- if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
9086
+ if (hostBehaviorsFor(runtime).permissionsSchema === 'claude' && settings.permissions) {
9092
9087
  let permissionsModified = false;
9093
9088
  if (Array.isArray(settings.permissions.allow)) {
9094
9089
  const before = settings.permissions.allow.length;
@@ -9159,7 +9154,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9159
9154
  // runtimes that host MCP there (Augment), symmetric to the mcp_config.json
9160
9155
  // removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is
9161
9156
  // removed — any other user-configured MCP servers are preserved.
9162
- if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' &&
9157
+ if (hostBehaviorsFor(runtime).mcpCompanion === 'settings-json' &&
9163
9158
  settings.mcpServers && typeof settings.mcpServers === 'object' &&
9164
9159
  settings.mcpServers.gsd !== undefined) {
9165
9160
  delete settings.mcpServers.gsd;
@@ -9848,7 +9843,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9848
9843
  // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
9849
9844
  // Claude local uses flatCommandsDir instead for manifest recording.
9850
9845
  const flatCommandsDir = path.join(configDir, 'commands');
9851
- const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command');
9846
+ const opencodeCommandDir = path.join(configDir, hostBehaviorsFor(runtime).flatCommandDir || 'command');
9852
9847
  // Hermes nests GSD skills under skills/gsd/ as a single category (#2841) —
9853
9848
  // already encoded in its layout descriptor's destSubpath ('skills/gsd').
9854
9849
  // All other runtimes that use the Codex-style skills layout use a flat skills/ root.
@@ -9863,7 +9858,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9863
9858
  // `options.scope` could drift; one cannot.
9864
9859
  const resolvedScope = options.scope === 'local' ? 'local' : 'global';
9865
9860
  const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
9866
- const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
9861
+ const codexSkillsManifestPrefix = hostBehaviorsFor(runtime).skillsManifestPrefix || 'skills/';
9867
9862
  // #3738: resolve the ACTUAL agents-install dir honoring an agents-kind `home`
9868
9863
  // override (antigravity global → $HOME/.gemini/config/agents), mirroring
9869
9864
  // _resolveSkillsRootDir for skills. Hardcoding configDir/agents left the
@@ -9890,6 +9885,11 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9890
9885
  // it from the directory it happened to be found in (#2872).
9891
9886
  runtime,
9892
9887
  scope: resolvedScope,
9888
+ // #4377: a surface re-apply is a separate process and cannot rely on the
9889
+ // installer's environment. Persist only a safe project-relative prefix.
9890
+ relativeIncludePrefix: resolvedScope === 'local' && hasRelativeIncludes
9891
+ ? runtimeArtifactConversion._projectRelativePrefixFromProjectRoot(process.cwd(), configDir)
9892
+ : undefined,
9893
9893
  files: {},
9894
9894
  };
9895
9895
 
@@ -9910,26 +9910,26 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9910
9910
  // Claude local (#1367): flat gsd-*.md files at commands/ level.
9911
9911
  // Only claude local writes gsd-*.md here; global installs don't emit commands,
9912
9912
  // so this branch is a no-op for global (no matching files to find).
9913
- if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
9913
+ if (hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
9914
9914
  for (const file of fs.readdirSync(flatCommandsDir)) {
9915
9915
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
9916
9916
  manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
9917
9917
  }
9918
9918
  }
9919
9919
  }
9920
- if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
9920
+ if (hostBehaviorsFor(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
9921
9921
  // #2329: derive the manifest key prefix from the SAME descriptor value used
9922
9922
  // to compute opencodeCommandDir above, instead of a separately-hardcoded
9923
9923
  // literal — a divergence here would silently break the manifest even after
9924
9924
  // the destSubpath descriptor is corrected (Generative Fix Divergence guard).
9925
- const flatCommandDirPrefix = _hostBehaviors(runtime).flatCommandDir || 'command';
9925
+ const flatCommandDirPrefix = hostBehaviorsFor(runtime).flatCommandDir || 'command';
9926
9926
  for (const file of fs.readdirSync(opencodeCommandDir)) {
9927
9927
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
9928
9928
  manifest.files[flatCommandDirPrefix + '/' + file] = fileHash(path.join(opencodeCommandDir, file));
9929
9929
  }
9930
9930
  }
9931
9931
  }
9932
- if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
9932
+ if (!hostBehaviorsFor(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
9933
9933
  // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
9934
9934
  const skillListPrefix = 'gsd-';
9935
9935
  for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
@@ -9940,14 +9940,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9940
9940
  }
9941
9941
  }
9942
9942
  // Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
9943
- if (_hostBehaviors(runtime).trackCategoryDescription) {
9943
+ if (hostBehaviorsFor(runtime).trackCategoryDescription) {
9944
9944
  const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
9945
9945
  if (fs.existsSync(descPath)) {
9946
9946
  manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
9947
9947
  }
9948
9948
  }
9949
9949
  }
9950
- if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
9950
+ if (hostBehaviorsFor(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
9951
9951
  const agentHashes = generateManifest(agentsDir);
9952
9952
  for (const [rel, hash] of Object.entries(agentHashes)) {
9953
9953
  const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md';
@@ -9968,7 +9968,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9968
9968
  // marker block, not the per-configDir manifest, since it lives outside it.)
9969
9969
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
9970
9970
  // hostBehaviors.clineRulesSurface.
9971
- if (_hostBehaviors(runtime).clineRulesSurface) {
9971
+ if (hostBehaviorsFor(runtime).clineRulesSurface) {
9972
9972
  for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) {
9973
9973
  const dest = path.join(configDir, rel);
9974
9974
  if (fs.existsSync(dest)) {
@@ -9989,7 +9989,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9989
9989
  // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
9990
9990
  // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
9991
9991
  // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
9992
- if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
9992
+ if (!isCodex && hostBehaviorsFor(runtime).skipSharedHooksInstall !== true) {
9993
9993
  // #3023: manifest keys must track the bundle wherever the descriptor put it,
9994
9994
  // or uninstall/saveLocalPatches silently orphan the tree.
9995
9995
  const sharedHooksDirName = resolveSharedHooksDirName(runtime);
@@ -10056,7 +10056,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10056
10056
 
10057
10057
  // Track the OpenCode native plugin adapter (#1914) so update/drift detection
10058
10058
  // and uninstall can account for it.
10059
- const _npM = _hostBehaviors(runtime).nativePlugin;
10059
+ const _npM = hostBehaviorsFor(runtime).nativePlugin;
10060
10060
  if (_npM) {
10061
10061
  const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file);
10062
10062
  if (fs.existsSync(pluginInstallPath)) {
@@ -10287,7 +10287,7 @@ function saveLocalPatches(configDir, pristineCtx) {
10287
10287
  skillsRoot !== resolvedConfig &&
10288
10288
  !skillsRoot.startsWith(resolvedConfig + path.sep)
10289
10289
  ) {
10290
- const prefix = _hostBehaviors(patchRuntime).skillsManifestPrefix || 'skills/';
10290
+ const prefix = hostBehaviorsFor(patchRuntime).skillsManifestPrefix || 'skills/';
10291
10291
  skillsRedirect = { root: skillsRoot, prefix };
10292
10292
  }
10293
10293
  }
@@ -10510,7 +10510,7 @@ function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
10510
10510
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
10511
10511
 
10512
10512
  if (meta.files && meta.files.length > 0) {
10513
- const reapplyCommand = _hostBehaviors(runtime).reapplyCommand || '/gsd-update --reapply';
10513
+ const reapplyCommand = hostBehaviorsFor(runtime).reapplyCommand || '/gsd-update --reapply';
10514
10514
  console.log('');
10515
10515
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
10516
10516
  for (const f of meta.files) {
@@ -10538,12 +10538,12 @@ function reportInstallerMigrationResult(result) {
10538
10538
 
10539
10539
  function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10540
10540
  // #2093: isKilo dropped — Kilo's agent/model-override handling below reads
10541
- // _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag.
10541
+ // hostBehaviorsFor(runtime).frontmatterDialect === 'kilo' instead of this flag.
10542
10542
  // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
10543
10543
  // settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards
10544
10544
  // below were removed, leaving isKimi unused in this function (the kimi
10545
10545
  // local-install-deferred branch above already reads
10546
- // _hostBehaviors(runtime).localInstallDeferred instead of this flag).
10546
+ // hostBehaviorsFor(runtime).localInstallDeferred instead of this flag).
10547
10547
  // #2096: isAntigravity dropped — antigravity's agents were already
10548
10548
  // descriptor-driven (installRuntimeArtifacts), so its two legacy-agent-loop
10549
10549
  // branches (the path-rewrite skip and the converter dispatch) were
@@ -10583,7 +10583,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10583
10583
  // re-running install()) can warn again if the same condition recurs.
10584
10584
  _codexResolverModelOmittedWarned = false;
10585
10585
 
10586
- if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
10586
+ if (hostBehaviorsFor(runtime).localInstallDeferred && !isGlobal) {
10587
10587
  console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
10588
10588
  console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
10589
10589
  console.log(` Project-level Kimi install semantics remain deferred.`);
@@ -10654,7 +10654,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10654
10654
  // auto-removed on reinstall (dual-read fallback per issue #791 spec).
10655
10655
  const targetDir = isGlobal
10656
10656
  ? getGlobalConfigDir(runtime, explicitConfigDir)
10657
- : _hostBehaviors(runtime).localTargetIsProjectRoot
10657
+ : hostBehaviorsFor(runtime).localTargetIsProjectRoot
10658
10658
  ? process.cwd()
10659
10659
  : path.join(process.cwd(), dirName);
10660
10660
 
@@ -10769,10 +10769,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10769
10769
  const isWindowsHost = process.platform === 'win32';
10770
10770
  const pathPrefix = computePathPrefix({
10771
10771
  isGlobal,
10772
- isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true,
10772
+ isOpencode: hostBehaviorsFor(runtime).skipHomePrefixSubstitution === true,
10773
10773
  isWindowsHost,
10774
10774
  resolvedTarget,
10775
10775
  homeDir,
10776
+ // #4377: the runtime's own localConfigDir. This is the prefix that reaches
10777
+ // copyWithPathReplacement, i.e. the one actually written into every
10778
+ // emitted command/skill/workflow body — the rewrite-engine seams below
10779
+ // handle re-applied surfaces, not the first install.
10780
+ localDirName: hostBehaviorsFor(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
10776
10781
  });
10777
10782
 
10778
10783
  // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
@@ -10783,6 +10788,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10783
10788
 
10784
10789
  // Track installation failures
10785
10790
  const failures = [];
10791
+ const configuredEntrypoints = [];
10786
10792
  let installerMigrationResult = null;
10787
10793
  const rollbackInstallerMigrations = () => {
10788
10794
  if (!installerMigrationResult || typeof installerMigrationResult.rollback !== 'function') return;
@@ -10835,7 +10841,48 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10835
10841
  // Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
10836
10842
  const codexPreInstallAgentContents = new Map();
10837
10843
  let codexPreInstallVersionBytes = null;
10838
- if (_hostBehaviors(runtime).tomlConfigInstall && !isMinimalMode(_effectiveInstallMode)) {
10844
+ // #4544 — manifest-driven snapshot state (captured in the block below):
10845
+ // codexPreInstallManagedFiles — Map<normalizedRelPath, Buffer|null>; one
10846
+ // entry per path the PRIOR install's gsd-file-manifest.json recorded.
10847
+ // null means the path did not exist pre-install, so rollback re-deletes
10848
+ // whatever this install put there instead of resurrecting it.
10849
+ // codexPreInstallManifestBytes — Buffer (or null) of the prior manifest file
10850
+ // itself, which a reinstall rewrites.
10851
+ // codexPreInstallHooksTree — Map<relPath, Buffer>, a full recursive
10852
+ // snapshot of <targetDir>/hooks/. The Codex manifest deliberately omits
10853
+ // hooks/ (the !isCodex gate on shared-hooks tracking), and hooks/ is
10854
+ // shared space, so the restore is wholesale: user files that predate
10855
+ // the install are in the snapshot and come back; anything the failed
10856
+ // install staged does not.
10857
+ const codexPreInstallManagedFiles = new Map();
10858
+ let codexPreInstallManifestBytes = null;
10859
+ const codexPreInstallHooksTree = new Map();
10860
+ // #4544 (review) — capture-state flags the restore must consult:
10861
+ // codexManagedSnapshotCaptured — the capture gate ran at all. When
10862
+ // false (non-Codex runtimes, minimal mode) NO pre-install state was
10863
+ // recorded, and the only safe restore is no restore: an empty
10864
+ // snapshot must never be read as "hooks/ was absent".
10865
+ // codexPreInstallHooksDirPreExisted — hooks/ existed as a DIRECTORY
10866
+ // pre-install. A pre-existing hooks FILE is left alone on rollback
10867
+ // rather than deleted.
10868
+ // codexPreInstallHooksCaptureIncomplete — some part of the hooks/ tree
10869
+ // could not be read (permissions, special files). The restore
10870
+ // downgrades to per-file so an uncapturable user file is never
10871
+ // destroyed by a wholesale delete whose snapshot lacked it.
10872
+ let codexManagedSnapshotCaptured = false;
10873
+ // null = the gate never ran; true/false = the gate ran and hooks/ (did|did
10874
+ // not) exist as a directory pre-install. Two states are load-bearing: a
10875
+ // clean first install records false, so its rollback removes the staged
10876
+ // hooks/ tree entirely; a non-Codex runtime records null, so rollback does
10877
+ // nothing.
10878
+ let codexPreInstallHooksDirPreExisted = null;
10879
+ let codexPreInstallHooksCaptureIncomplete = false;
10880
+ // #4249 CR: not gated on install mode. restoreCodexSnapshot is reachable for
10881
+ // a core/--minimal install too (#2695), and its pass-2 sweeps remove every
10882
+ // gsd-* skill dir / agent file the snapshot does not claim — so an empty
10883
+ // minimal-mode snapshot deleted the whole surface with nothing to restore.
10884
+ if (hostBehaviorsFor(runtime).tomlConfigInstall) {
10885
+ codexManagedSnapshotCaptured = true;
10839
10886
  const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
10840
10887
  if (fs.existsSync(_preSkillsDir)) {
10841
10888
  for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
@@ -10877,19 +10924,213 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10877
10924
  if (fs.existsSync(_preVersionPath)) {
10878
10925
  try { codexPreInstallVersionBytes = fs.readFileSync(_preVersionPath); } catch (_) { /* best-effort */ }
10879
10926
  }
10880
- }
10927
+ // #4544 — capture the manifest-driven surfaces, same best-effort
10928
+ // conventions as the skills/ snapshot above. readInstallManifest is the
10929
+ // same hardened reader installer-migrations uses (array/ garbage shapes
10930
+ // degrade to an empty file set — rollback then simply covers less, never
10931
+ // crashes), and resolveInstallRelativePath keeps a hostile manifest key
10932
+ // from turning into a write outside the install root.
10933
+ const _priorManifest = readInstallManifest(targetDir);
10934
+ for (const rel of Object.keys(_priorManifest.files)) {
10935
+ const resolved = resolveInstallRelativePath(targetDir, rel);
10936
+ if (!resolved) continue;
10937
+ try {
10938
+ codexPreInstallManagedFiles.set(resolved.relPath, fs.readFileSync(resolved.fullPath));
10939
+ } catch (_) {
10940
+ // Listed but absent/unreadable pre-install: snapshot absence, so
10941
+ // rollback re-deletes instead of resurrecting.
10942
+ codexPreInstallManagedFiles.set(resolved.relPath, null);
10943
+ }
10944
+ }
10945
+ const _preManifestPath = path.join(targetDir, MANIFEST_NAME);
10946
+ if (fs.existsSync(_preManifestPath)) {
10947
+ try { codexPreInstallManifestBytes = fs.readFileSync(_preManifestPath); } catch (_) { /* best-effort */ }
10948
+ }
10949
+ // #4544 (review) — a clean FIRST install has no prior manifest, so nothing
10950
+ // above records the payload this install is about to write, and a failed
10951
+ // clean install would roll back to a half-written tree. Enumerate the SAME
10952
+ // source directories the installer copies (a directory walk tracks the
10953
+ // source tree automatically — no second file list to keep in parity) and
10954
+ // record every path as absent-pre-install. On a reinstall most of these
10955
+ // already carry entries from the prior manifest; any that do not (files
10956
+ // new in this version) snapshot their pre-install bytes or absence exactly
10957
+ // like the rest, which also closes the new-version-file residual.
10958
+ const _recordWritePlanTree = (srcDir, relPrefix) => {
10959
+ let children;
10960
+ try { children = fs.readdirSync(srcDir, { withFileTypes: true }); } catch (_) { return; }
10961
+ for (const child of children) {
10962
+ const rel = relPrefix ? `${relPrefix}/${child.name}` : child.name;
10963
+ if (child.isDirectory()) {
10964
+ _recordWritePlanTree(path.join(srcDir, child.name), rel);
10965
+ } else if (child.isFile()) {
10966
+ if (codexPreInstallManagedFiles.has(rel)) continue;
10967
+ // USER_OWNED_ARTIFACTS are manifest-relative to gsd-core/ (#2771):
10968
+ // they are durably staged across reinstalls and must never enter a
10969
+ // rollback delete-set.
10970
+ const manifestRel = rel.startsWith('gsd-core/') ? rel.slice('gsd-core/'.length) : rel;
10971
+ if (USER_OWNED_ARTIFACTS.includes(manifestRel)) continue;
10972
+ const resolved = resolveInstallRelativePath(targetDir, rel);
10973
+ if (!resolved) continue;
10974
+ try {
10975
+ codexPreInstallManagedFiles.set(rel, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
10976
+ } catch (_) {
10977
+ codexPreInstallManagedFiles.set(rel, null);
10978
+ }
10979
+ }
10980
+ }
10981
+ };
10982
+ _recordWritePlanTree(path.join(src, 'gsd-core'), 'gsd-core');
10983
+ _recordWritePlanTree(path.join(src, 'scripts', 'changeset'), 'scripts/changeset');
10984
+ _recordWritePlanTree(path.join(src, 'scripts', 'lib'), 'scripts/lib');
10985
+ // gsd-core/CHANGELOG.md is sourced from the repo root (not src/gsd-core)
10986
+ // and gsd-core/.gsd-runtime is generated at install time — neither appears
10987
+ // in the directory walks, so record them explicitly.
10988
+ 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']) {
10989
+ if (codexPreInstallManagedFiles.has(standalone)) continue;
10990
+ const resolved = resolveInstallRelativePath(targetDir, standalone);
10991
+ if (!resolved) continue;
10992
+ try {
10993
+ codexPreInstallManagedFiles.set(standalone, fs.existsSync(resolved.fullPath) ? fs.readFileSync(resolved.fullPath) : null);
10994
+ } catch (_) {
10995
+ codexPreInstallManagedFiles.set(standalone, null);
10996
+ }
10997
+ }
10998
+ // hooks/ — full recursive snapshot, but never blind: lstat every entry so
10999
+ // a symlink under hooks/ is neither followed (a link to a FIFO would hang
11000
+ // the installer, /dev/zero would exhaust memory, and a link to private
11001
+ // data would copy that data into the snapshot — hooks/ is user-writable
11002
+ // shared space and, for local installs, repo-controllable) nor restored
11003
+ // as a link. Anything unreadable or special marks the capture INCOMPLETE
11004
+ // so the restore downgrades to per-file instead of wholesale-deleting a
11005
+ // tree it never fully saw. A pre-existing hooks FILE (not directory) is
11006
+ // recorded as such and left alone on rollback.
11007
+ const _preHooksPath = path.join(targetDir, 'hooks');
11008
+ let _preHooksStat = null;
11009
+ try { _preHooksStat = fs.lstatSync(_preHooksPath); } catch (_) { /* absent */ }
11010
+ codexPreInstallHooksDirPreExisted = Boolean(_preHooksStat && _preHooksStat.isDirectory());
11011
+ if (codexPreInstallHooksDirPreExisted) {
11012
+ const _snapshotHooksDir = (dir, relBase) => {
11013
+ let children;
11014
+ try { children = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) {
11015
+ codexPreInstallHooksCaptureIncomplete = true;
11016
+ return;
11017
+ }
11018
+ for (const child of children) {
11019
+ const relPath = relBase ? `${relBase}/${child.name}` : child.name;
11020
+ const fullPath = path.join(dir, child.name);
11021
+ let st = null;
11022
+ try { st = fs.lstatSync(fullPath); } catch (_) {
11023
+ codexPreInstallHooksCaptureIncomplete = true;
11024
+ continue;
11025
+ }
11026
+ if (st.isDirectory()) {
11027
+ _snapshotHooksDir(fullPath, relPath);
11028
+ } else if (st.isFile()) {
11029
+ try { codexPreInstallHooksTree.set(relPath, fs.readFileSync(fullPath)); } catch (_) {
11030
+ codexPreInstallHooksCaptureIncomplete = true;
11031
+ }
11032
+ } else {
11033
+ codexPreInstallHooksCaptureIncomplete = true;
11034
+ }
11035
+ }
11036
+ };
11037
+ _snapshotHooksDir(_preHooksPath, '');
11038
+ }
11039
+ }
11040
+
11041
+ // #4544 — shared restore for the manifest-driven surfaces. Called by BOTH
11042
+ // rollback paths: _codexPreConfigRollback (CHANGELOG.md, scripts/, the
11043
+ // initial manifest write AND — via installer migrations' stale-hook removal
11044
+ // — hooks/ itself are all mutated BEFORE config.toml is touched, so the
11045
+ // early path must cover them) and the full restoreCodexSnapshot() below.
11046
+ // Best-effort throughout, matching the #3245 convention: restore failures
11047
+ // never mask the original install error.
11048
+ const restoreCodexManagedSnapshot = () => {
11049
+ // #4544 (review) — if the capture never ran (non-Codex runtimes, minimal
11050
+ // mode), no pre-install state was recorded. The only safe action is NONE:
11051
+ // an empty snapshot must never be read as "hooks/ was absent", or a
11052
+ // minimal-mode rollback would delete the user's entire hooks/ tree.
11053
+ if (!codexManagedSnapshotCaptured) return;
11054
+ // hooks/ — the pre-install tree is restored wholesale: a user file that
11055
+ // predated the install is IN the snapshot and comes back; anything the
11056
+ // failed install staged is not, and goes away with the tree. When the
11057
+ // capture was INCOMPLETE, wholesale deletion would permanently destroy a
11058
+ // file whose bytes were never captured, so the restore downgrades to
11059
+ // per-file: put back what was captured and remove only the names GSD
11060
+ // itself stages (the hoisted CODEX_HOOKS_TO_COPY set plus the CommonJS
11061
+ // marker). hooks/lib/ is left untouched in that mode — its contents are
11062
+ // transitive and cannot be enumerated safely without the capture.
11063
+ if (codexPreInstallHooksDirPreExisted !== null) {
11064
+ const _hooksRestoreDir = path.join(targetDir, 'hooks');
11065
+ if (!codexPreInstallHooksDirPreExisted) {
11066
+ // Clean first install: nothing pre-existed under hooks/, so nothing
11067
+ // the failed install staged may survive either.
11068
+ try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
11069
+ } else if (!codexPreInstallHooksCaptureIncomplete) {
11070
+ try { fs.rmSync(_hooksRestoreDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
11071
+ for (const [relPath, buf] of codexPreInstallHooksTree) {
11072
+ const destFile = path.join(_hooksRestoreDir, relPath);
11073
+ try {
11074
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
11075
+ fs.writeFileSync(destFile, buf);
11076
+ } catch (_) { /* best-effort */ }
11077
+ }
11078
+ } else {
11079
+ // GSD-owned names are removed FIRST: several of them are also
11080
+ // legitimate pre-install files the snapshot just restored, and a
11081
+ // removal pass after the restore would delete the restored bytes.
11082
+ for (const hookName of CODEX_HOOKS_TO_COPY) {
11083
+ try { fs.rmSync(path.join(_hooksRestoreDir, hookName), { force: true }); } catch (_) { /* best-effort */ }
11084
+ }
11085
+ try { fs.rmSync(path.join(_hooksRestoreDir, 'package.json'), { force: true }); } catch (_) { /* best-effort */ }
11086
+ for (const [relPath, buf] of codexPreInstallHooksTree) {
11087
+ const destFile = path.join(_hooksRestoreDir, relPath);
11088
+ try {
11089
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
11090
+ fs.writeFileSync(destFile, buf);
11091
+ } catch (_) { /* best-effort */ }
11092
+ }
11093
+ }
11094
+ }
11095
+ // Every GSD-owned path the prior manifest recorded (plus the clean-install
11096
+ // write plan): restore bytes, or re-delete a path that was absent
11097
+ // pre-install.
11098
+ for (const [relPath, buf] of codexPreInstallManagedFiles) {
11099
+ const resolved = resolveInstallRelativePath(targetDir, relPath);
11100
+ if (!resolved) continue;
11101
+ try {
11102
+ if (buf !== null) {
11103
+ fs.mkdirSync(path.dirname(resolved.fullPath), { recursive: true });
11104
+ fs.writeFileSync(resolved.fullPath, buf);
11105
+ } else if (fs.existsSync(resolved.fullPath)) {
11106
+ fs.rmSync(resolved.fullPath, { force: true });
11107
+ }
11108
+ } catch (_) { /* best-effort */ }
11109
+ }
11110
+ // The prior manifest file itself: reinstall rewrites it; rollback returns
11111
+ // the previous install's manifest (or removes it on a clean first install).
11112
+ const _manifestRestorePath = path.join(targetDir, MANIFEST_NAME);
11113
+ if (codexPreInstallManifestBytes !== null) {
11114
+ try { fs.writeFileSync(_manifestRestorePath, codexPreInstallManifestBytes); } catch (_) { /* best-effort */ }
11115
+ } else if (fs.existsSync(_manifestRestorePath)) {
11116
+ try { fs.unlinkSync(_manifestRestorePath); } catch (_) { /* best-effort */ }
11117
+ }
11118
+ };
10881
11119
 
10882
11120
  // #3245 CR finding 2 — Rollback coverage extends to ALL post-snapshot operations,
10883
11121
  // not just the Codex config/hook error paths. Any throw between snapshot capture and
10884
11122
  // the Codex config block (skills copy, agents copy, VERSION write, manifest write, etc.)
10885
11123
  // must also trigger rollback so the caller is never left in a partially-installed state.
10886
11124
  //
10887
- // _codexPreConfigRollback covers the four surfaces that can be mutated before
10888
- // config.toml is touched: skills/, agents/, gsd-core/VERSION, and orphaned
11125
+ // _codexPreConfigRollback covers the surfaces that can be mutated before
11126
+ // config.toml is touched: skills/, agents/, gsd-core/VERSION, the manifest-
11127
+ // driven surfaces (#4544 — CHANGELOG.md, scripts/, .gsd-runtime and the
11128
+ // manifest itself are all rewritten in this window), and orphaned
10889
11129
  // atomic-write temp files. It is safe to call before any writes have happened.
10890
11130
  // The full restoreCodexSnapshot() (defined inside the config block) additionally
10891
- // handles config.toml, which is not yet touched at this point in the pipeline.
10892
- const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
11131
+ // handles config.toml and the staged hooks/ tree, which are not yet touched
11132
+ // at this point in the pipeline.
11133
+ const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
10893
11134
  rollbackInstallerMigrations();
10894
11135
  // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
10895
11136
  const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
@@ -10949,6 +11190,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10949
11190
  } else if (fs.existsSync(_earlyVersionPath)) {
10950
11191
  try { fs.unlinkSync(_earlyVersionPath); } catch (_) { /* best-effort */ }
10951
11192
  }
11193
+ // #4544 — manifest-driven surfaces (CHANGELOG.md, scripts/, the initial
11194
+ // manifest write, and — via installer migrations' stale-hook removal —
11195
+ // hooks/ itself are all mutated in this window). The shared restore is
11196
+ // also idempotent against an untouched tree.
11197
+ restoreCodexManagedSnapshot();
10952
11198
  // Orphaned atomic-write temp files.
10953
11199
  const _earlyTmpPattern = /\.tmp-\d+-\d+$/;
10954
11200
  function _earlyCleanTmpFiles(dir) {
@@ -11074,7 +11320,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11074
11320
  // used to describe. Claude-local remains the one special-cased path
11075
11321
  // (copyWithPathReplacement + stale-skills cleanup).
11076
11322
  const _isSkillsRuntime = (() => {
11077
- if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
11323
+ if (hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
11078
11324
  // #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
11079
11325
  // rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
11080
11326
  // — cline is the only declarant, capabilities/cline/capability.json) must not
@@ -11085,7 +11331,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11085
11331
  // spuriously fail; the `localCommandsViaRules` branch below (unchanged
11086
11332
  // messaging) and the unconditional agents-materialization block further down
11087
11333
  // (installAgentsKindStandalone) already cover this runtime/scope correctly.
11088
- if (!isGlobal && _hostBehaviors(runtime).localCommandsViaRules) return false;
11334
+ if (!isGlobal && hostBehaviorsFor(runtime).localCommandsViaRules) return false;
11089
11335
  const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
11090
11336
  const layout = cap && cap.runtime && cap.runtime.artifactLayout;
11091
11337
  if (!layout) return false;
@@ -11124,13 +11370,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11124
11370
  // marker. Write failure is non-fatal (install proceeds; warn so /gsd-surface breakage is
11125
11371
  // diagnosable) — the same contract the late write had.
11126
11372
  function _writeGsdSourceMarker(runtime, targetDir, src, isGlobal) {
11127
- if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
11373
+ if (hostBehaviorsFor(runtime).sourceMarkerFile && isGlobal) {
11128
11374
  const gsdSourceCommands = path.join(src, 'commands', 'gsd');
11129
11375
  if (fs.existsSync(gsdSourceCommands)) {
11130
11376
  try {
11131
11377
  // ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
11132
11378
  // must resolve under targetDir (parity with the other descriptor-driven writes).
11133
- const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
11379
+ const _markerPath = assertDestWithinConfigHome(targetDir, hostBehaviorsFor(runtime).sourceMarkerFile);
11134
11380
  if (hasExistingSymlinkBetween(path.resolve(targetDir), _markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
11135
11381
  throw new Error(`compatibility marker "${_markerPath}" contains an untrusted symlink`);
11136
11382
  }
@@ -11205,7 +11451,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11205
11451
  // index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
11206
11452
  // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
11207
11453
  // sufficient for Codex discovery. User-owned dirs are never touched.
11208
- if (_hostBehaviors(runtime).cleanupSkillSidecars) {
11454
+ if (hostBehaviorsFor(runtime).cleanupSkillSidecars) {
11209
11455
  cleanupCodexSkillMetadataSidecars(_skillsRootDir);
11210
11456
  }
11211
11457
 
@@ -11231,7 +11477,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11231
11477
  // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
11232
11478
  // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that
11233
11479
  // declares it, so this is byte-parity).
11234
- if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) {
11480
+ if (hostBehaviorsFor(runtime).legacyDevinSkillsCleanup && !isGlobal) {
11235
11481
  const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
11236
11482
  if (removedCount > 0) {
11237
11483
  console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
@@ -11239,12 +11485,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11239
11485
  }
11240
11486
 
11241
11487
  // Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
11242
- if (_hostBehaviors(runtime).writeCategoryDescription) {
11488
+ if (hostBehaviorsFor(runtime).writeCategoryDescription) {
11243
11489
  writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
11244
11490
  }
11245
11491
 
11246
11492
  // Verify installed artifacts and report
11247
- if (_hostBehaviors(runtime).reportSkillsCount) {
11493
+ if (hostBehaviorsFor(runtime).reportSkillsCount) {
11248
11494
  const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
11249
11495
  if (fs.existsSync(hermesSkillsDir)) {
11250
11496
  // Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
@@ -11258,7 +11504,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11258
11504
  } else {
11259
11505
  failures.push('skills/gsd/*');
11260
11506
  }
11261
- } else if (_hostBehaviors(runtime).verificationStyle === 'kimi') {
11507
+ } else if (hostBehaviorsFor(runtime).verificationStyle === 'kimi') {
11262
11508
  const skillsDir = path.join(targetDir, 'skills');
11263
11509
  const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
11264
11510
  if (fs.existsSync(skillsDir)) {
@@ -11282,7 +11528,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11282
11528
  // hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the
11283
11529
  // same mechanism the 'kimi' verificationStyle branch above uses; windsurf
11284
11530
  // is the only runtime that declares this value, so this is byte-parity).
11285
- } else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') {
11531
+ } else if (hostBehaviorsFor(runtime).verificationStyle === 'windsurf-workflows') {
11286
11532
  if (isGlobal) {
11287
11533
  console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
11288
11534
  } else {
@@ -11331,7 +11577,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11331
11577
  // Descriptor-driven commands/ output report (currently CodeBuddy).
11332
11578
  // Cursor retired this parallel surface in #2644 because its skills are
11333
11579
  // already slash-menu entries as well as model-invocable context.
11334
- if (_hostBehaviors(runtime).reportCommandsDir) {
11580
+ if (hostBehaviorsFor(runtime).reportCommandsDir) {
11335
11581
  const commandsDir = path.join(targetDir, 'commands');
11336
11582
  if (fs.existsSync(commandsDir)) {
11337
11583
  const cmdCount = fs.readdirSync(commandsDir)
@@ -11346,14 +11592,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11346
11592
  }
11347
11593
  }
11348
11594
  }
11349
- } else if (_hostBehaviors(runtime).localCommandsViaRules) {
11595
+ } else if (hostBehaviorsFor(runtime).localCommandsViaRules) {
11350
11596
  // Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
11351
11597
  // No skills/commands directory needed for local installs.
11352
11598
  // Global installs are handled above by _isSkillsRuntime (#782).
11353
11599
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
11354
11600
  // hostBehaviors.localCommandsViaRules.
11355
11601
  console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
11356
- } else if (_hostBehaviors(runtime).pluginOnlyInstall) {
11602
+ } else if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
11357
11603
  // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
11358
11604
  // registered programmatically by the native extension (pi/gsd.cjs →
11359
11605
  // extensions/gsd.js, staged separately below; the dest suffix must be
@@ -11461,8 +11707,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11461
11707
  // double-stages their plugin file. A runtime like pi, whose artifactLayout is
11462
11708
  // intentionally empty for both scopes (`_isSkillsRuntime` is false), still
11463
11709
  // needs its declared hostBehaviors.nativePlugin file copied into targetDir.
11464
- if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) {
11465
- _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
11710
+ if (!_isSkillsRuntime && hostBehaviorsFor(runtime).nativePlugin) {
11711
+ _installNativePluginIfDeclared(runtime, targetDir, hostBehaviorsFor(runtime), src);
11466
11712
  }
11467
11713
 
11468
11714
  // #2624: the .gsd-source marker is now written by _writeGsdSourceMarker()
@@ -11483,7 +11729,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11483
11729
  // hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the
11484
11730
  // only runtime that declares it, so this is byte-parity — the #1629 fix
11485
11731
  // itself is unchanged).
11486
- if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
11732
+ if (hostBehaviorsFor(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
11487
11733
  const commandsSrc = path.join(src, 'commands', 'gsd');
11488
11734
  const commandsDest = path.join(skillDest, 'commands', 'gsd');
11489
11735
  if (fs.existsSync(commandsSrc)) {
@@ -11547,7 +11793,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11547
11793
  // scopes (programmatic dispatch, no named-dispatch subagent toolkit, no
11548
11794
  // host-read markdown surface), so the resolved layout has no `agents` kind
11549
11795
  // to stage and the function returns `null` without writing anything.
11550
- if (_hostBehaviors(runtime).pluginOnlyInstall) {
11796
+ if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
11551
11797
  console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
11552
11798
  } else if (_isSkillsRuntime) {
11553
11799
  console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
@@ -11582,7 +11828,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11582
11828
  // advertising the old full agent surface even though the descriptor-driven
11583
11829
  // write above already skipped writing the .md files for a minimal-tier
11584
11830
  // resolvedProfile. Reuse the same helper that powers `--uninstall`.
11585
- if (isMinimalMode(_effectiveInstallMode) && _hostBehaviors(runtime).tomlConfigInstall) {
11831
+ if (isMinimalMode(_effectiveInstallMode) && hostBehaviorsFor(runtime).tomlConfigInstall) {
11586
11832
  const codexConfigPath = path.join(targetDir, 'config.toml');
11587
11833
  if (fs.existsSync(codexConfigPath)) {
11588
11834
  const existing = fs.readFileSync(codexConfigPath, 'utf8');
@@ -11701,7 +11947,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11701
11947
  // runtime.hostBehaviors.brandingRewrites. This site only
11702
11948
  // rewrites the two brand-name keys (no `.claude/` here — the
11703
11949
  // config-dir replace above already handled path fragments).
11704
- const _b2 = _hostBehaviors(runtime).brandingRewrites;
11950
+ const _b2 = hostBehaviorsFor(runtime).brandingRewrites;
11705
11951
  if (_b2) {
11706
11952
  content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']);
11707
11953
  content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']);
@@ -11809,7 +12055,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11809
12055
  console.log(` ${green}✓${reset} Wrote ${sharedHooksDirName}/package.json (CommonJS mode)`);
11810
12056
  break;
11811
12057
  case 'preserved-foreign':
11812
- console.warn(` ${yellow}⚠${reset} Left existing ${sharedHooksDirName}/package.json untouched (not GSD's marker) — GSD hooks may not resolve as CommonJS`);
12058
+ // #4759: the foreign file usually DOES declare "type": "commonjs" —
12059
+ // any hand-written or formatter-touched package.json does — and Node
12060
+ // then loads the staged .js hooks as CommonJS, so the old
12061
+ // unconditional "may not resolve" claim was usually false. The
12062
+ // sibling plugin path (src/install-engine.cts) words this same
12063
+ // outcome conditionally; match it and keep will-not-load conditional
12064
+ // on "type": "module", the only case where it is true.
12065
+ 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.`);
11813
12066
  break;
11814
12067
  case 'failed':
11815
12068
  // Best-effort: a read-only or full config dir must not abort the
@@ -11853,7 +12106,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11853
12106
  // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
11854
12107
  // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
11855
12108
  // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
11856
- if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
12109
+ if (!isCodex && hostBehaviorsFor(runtime).skipSharedHooksInstall !== true) {
11857
12110
  if (!installSharedHooksBundle(targetDir)) {
11858
12111
  failures.push('hooks');
11859
12112
  }
@@ -12020,7 +12273,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12020
12273
  }
12021
12274
 
12022
12275
  // Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
12023
- if (!_hostBehaviors(runtime).ownsClaudePaths) {
12276
+ if (!hostBehaviorsFor(runtime).ownsClaudePaths) {
12024
12277
  const leakedPaths = [];
12025
12278
  // Only scan files that were written by this install (manifest-tracked).
12026
12279
  // Scanning the entire targetDir can match user-authored content that
@@ -12041,6 +12294,42 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12041
12294
  manifestFiles = null;
12042
12295
  }
12043
12296
  if (manifestFiles !== null) {
12297
+ // #4667: codex-installed artifacts must not keep `@~/.claude/gsd-core/…`
12298
+ // include references — the `@` form resolves into the CLAUDE install
12299
+ // (wrong copy on dual-runtime machines at divergent versions, nothing at
12300
+ // all on codex-only ones; #570 cause 2 residue). Every target ships in
12301
+ // the codex install, so rewriting the `@~/` include form to the codex
12302
+ // root is mechanical and correct. This runs after all .md emitters
12303
+ // (several bypass the per-runtime converters — that is how the leak
12304
+ // survived the per-emitter fixes; the agent .tomls are generated later
12305
+ // and prefix themselves), and before the scan below, which stays as the
12306
+ // verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
12307
+ // fallback chains and prose `.claude` mentions carry no `@~/` prefix and
12308
+ // are deliberately untouched, as is CHANGELOG.md.
12309
+ if (hostBehaviorsFor(runtime).rewriteClaudeAtIncludes) {
12310
+ for (const relPath of manifestFiles) {
12311
+ const fileName = path.basename(relPath);
12312
+ if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
12313
+ if (fileName === 'CHANGELOG.md') continue;
12314
+ const rewritePath = path.join(targetDir, relPath);
12315
+ let rewriteContent;
12316
+ try {
12317
+ rewriteContent = fs.readFileSync(rewritePath, 'utf8');
12318
+ } catch (rewriteErr) {
12319
+ continue; // inaccessible or missing — the scan below reports or skips it
12320
+ }
12321
+ const rewritten = rewriteContent
12322
+ .split('@~/.claude/gsd-core/').join('@~/.codex/gsd-core/')
12323
+ .split('@$HOME/.claude/gsd-core/').join('@$HOME/.codex/gsd-core/');
12324
+ if (rewritten !== rewriteContent) {
12325
+ try {
12326
+ fs.writeFileSync(rewritePath, rewritten);
12327
+ } catch (writeErr) {
12328
+ continue; // never fail the install over the rewrite; the scan still warns
12329
+ }
12330
+ }
12331
+ }
12332
+ }
12044
12333
  for (const relPath of manifestFiles) {
12045
12334
  const fileName = path.basename(relPath);
12046
12335
  if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
@@ -12249,6 +12538,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12249
12538
  try { fs.unlinkSync(_rollbackVersionPath); } catch (_) { /* best-effort */ }
12250
12539
  }
12251
12540
 
12541
+ // 4b. #4544 — manifest-driven surfaces: the staged hooks/ tree, every
12542
+ // GSD-owned path the prior manifest recorded (scripts/, gsd-core/
12543
+ // payload), and the prior manifest file itself.
12544
+ restoreCodexManagedSnapshot();
12545
+
12252
12546
  // 5. Orphaned atomic-write temp files (<file>.tmp-<pid>-<n>) in targetDir.
12253
12547
  // These can accumulate if an atomic write fails mid-rename. Best-effort scan.
12254
12548
  //
@@ -12322,11 +12616,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12322
12616
  // ENOENT -> allow(undefined), every invocation, every event, no exceptions).
12323
12617
  // A pre-#2586 install's stale copy + hooks.json registrations are cleaned
12324
12618
  // up below (see the CODEX_EXTENDED_HOOK_EVENTS loop), not re-added here.
12325
- const CODEX_HOOKS_TO_COPY = [
12326
- 'gsd-check-update.js',
12327
- 'gsd-check-update-worker.js',
12328
- 'managed-hooks-registry.cjs',
12329
- ];
12619
+ // CODEX_HOOKS_TO_COPY itself lives at module scope (#4544) — the rollback's
12620
+ // incomplete-capture path must name the same set without a second literal.
12330
12621
  const codexHooksSrc = path.join(src, 'hooks', 'dist');
12331
12622
  if (fs.existsSync(codexHooksSrc)) {
12332
12623
  const codexHooksDest = path.join(targetDir, 'hooks');
@@ -12520,6 +12811,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12520
12811
  absoluteRunner: codexNodeRunner,
12521
12812
  platform: process.platform,
12522
12813
  });
12814
+ configuredEntrypoints.push(...(hookWrite.configuredEntrypoints || []));
12523
12815
  if (hookWrite.wrote) {
12524
12816
  console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart via hooks.json)`);
12525
12817
  } else {
@@ -12599,7 +12891,21 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12599
12891
  }
12600
12892
 
12601
12893
  persistActiveProfileMarker();
12602
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
12894
+ // #4249: expose restoreCodexSnapshot (#3245) as a SECOND, separately named
12895
+ // rollback rather than rebinding `rollbackInstallerMigrations` to it. A
12896
+ // configured-entrypoint validation failure discovered later (outside this
12897
+ // function, after Codex's own hooks.json/config.toml write already
12898
+ // succeeded) previously had only the installer-migrations closure to call,
12899
+ // leaving the just-written config.toml/hooks.json broken on disk despite
12900
+ // Codex already owning a full pre-install snapshot/restore for exactly this.
12901
+ //
12902
+ // Every runtime's `rollbackInstallerMigrations` therefore still means what
12903
+ // it says — the installer-migrations-only closure, which is what a
12904
+ // finalize-stage failure that is NOT an entrypoint-validation failure gets
12905
+ // (the Phase 4 contract). `rollbackPreInstallSnapshot` is Codex-only and is
12906
+ // chosen only for entrypoint-validation failures. See the selection in
12907
+ // installAllRuntimes' rollbackFinalizedInstallerMigrations.
12908
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations, rollbackPreInstallSnapshot: restoreCodexSnapshot };
12603
12909
  }
12604
12910
 
12605
12911
  if (plan.installSurface === 'copilot-instructions') {
@@ -12629,7 +12935,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12629
12935
  writeCopilotHookConfig(targetDir);
12630
12936
  console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`);
12631
12937
  persistActiveProfileMarker();
12632
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
12938
+ // #4249: `[]`, not omitted — every Copilot hook is an inline `printf`
12939
+ // one-liner (GSD_COPILOT_*_HOOK_BASH/PWSH in src/runtime-hooks-surface.cts),
12940
+ // so this runtime genuinely launches no GSD-managed script and has no
12941
+ // interpreter to resolve. Stated explicitly like every other branch rather
12942
+ // than leaning on installAllRuntimes' `|| []` defence.
12943
+ // #4249 (antigravity review): `rollbackInstallerMigrations` was missing here
12944
+ // — every other branch returns it. This PR's own aggregate entrypoint gate
12945
+ // is what makes the gap reachable: an unrelated runtime's invalid entrypoint
12946
+ // now triggers rollbackFinalizedInstallerMigrations for every result in the
12947
+ // batch, and a Copilot result with no rollback function silently skips
12948
+ // reverting Copilot's own installer migrations.
12949
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
12633
12950
  }
12634
12951
 
12635
12952
  if (plan.installSurface === 'cursor-hooks-json') {
@@ -12638,7 +12955,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12638
12955
  // stop, subagentStart, subagentStop) via runtime-hooks-surface.cts, which reads
12639
12956
  // the event list from the descriptor-driven adapter module.
12640
12957
  const cursorHookResult = writeCursorHooksJson(targetDir, src, {
12641
- managedHookEvents: _hostBehaviors(runtime).managedHookEvents,
12958
+ managedHookEvents: hostBehaviorsFor(runtime).managedHookEvents,
12642
12959
  });
12643
12960
  if (cursorHookResult.changed) {
12644
12961
  console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop)`);
@@ -12652,7 +12969,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12652
12969
  // The re-run is retained for parity with the settings.json install path.
12653
12970
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
12654
12971
  persistActiveProfileMarker();
12655
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
12972
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: cursorHookResult.configuredEntrypoints, rollbackInstallerMigrations };
12656
12973
  }
12657
12974
 
12658
12975
  if (plan.installSurface === 'profile-marker-only') {
@@ -12708,6 +13025,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12708
13025
  const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
12709
13026
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
12710
13027
  const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
13028
+ configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
12711
13029
  if (kimiHooksResult.changed) {
12712
13030
  console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
12713
13031
  }
@@ -12730,9 +13048,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12730
13048
  // reports success and the user is left with the very breakage the opt-in
12731
13049
  // exists to prevent. Verified reproducible before this guard existed.
12732
13050
  const kimiInstalledThisRun = selectedRuntimes.includes('kimi');
12733
- if (hasReclaimKimiLegacy && runtime === 'kimi-code' && kimiInstalledThisRun) {
13051
+ if (hasReclaimKimiLegacy && hostBehaviorsFor(runtime).reclaimsKimiLegacyHooksRoot && kimiInstalledThisRun) {
12734
13052
  console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: this run also installs --kimi, so ${resolveKimiHooksTomlDir({ runtime: 'kimi' })} is a live Kimi CLI install`);
12735
- } else if (hasReclaimKimiLegacy && runtime === 'kimi-code') {
13053
+ } else if (hasReclaimKimiLegacy && hostBehaviorsFor(runtime).reclaimsKimiLegacyHooksRoot) {
12736
13054
  const legacyKimiRoot = resolveKimiHooksTomlDir({ runtime: 'kimi' });
12737
13055
  // Both roots honor their own env override (KIMI_SHARE_DIR /
12738
13056
  // KIMI_CODE_HOME). A user who points both at ONE directory collapses
@@ -12764,6 +13082,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12764
13082
  const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
12765
13083
  platform: process.platform,
12766
13084
  });
13085
+ configuredEntrypoints.push(...windsurfHookResult.configuredEntrypoints);
12767
13086
  if (windsurfHookResult.changed) {
12768
13087
  console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
12769
13088
  } else {
@@ -12779,19 +13098,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12779
13098
  }
12780
13099
 
12781
13100
  persistActiveProfileMarker();
12782
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13101
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints, rollbackInstallerMigrations };
12783
13102
  }
12784
13103
 
12785
13104
  if (plan.installSurface === 'cline-rules') {
12786
13105
  // Cline uses the `.clinerules/` directory form (issue #787): GSD rules live
12787
13106
  // at .clinerules/gsd.md and a PreToolUse lifecycle hook at
12788
13107
  // .clinerules/hooks/PreToolUse. Global installs also get ~/.agents/AGENTS.md.
12789
- writeClineArtifacts(targetDir, isGlobal);
13108
+ const clineArtifacts = writeClineArtifacts(targetDir, isGlobal);
12790
13109
  // Re-run the manifest pass: these artifacts are written *after* the earlier
12791
13110
  // writeManifest() call, so a second pass is needed to hash-track them.
12792
13111
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
12793
13112
  persistActiveProfileMarker();
12794
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13113
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: clineArtifacts.configuredEntrypoints, rollbackInstallerMigrations };
12795
13114
  }
12796
13115
 
12797
13116
  // Configure statusline and hooks in settings.json (or settings.local.json for local Claude installs).
@@ -12807,15 +13126,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12807
13126
  // #2870: the CURRENT scope's settings filename is sourced from the Install
12808
13127
  // Scope Module (_installScope.settingsFile, resolveScope's per-scope field)
12809
13128
  // instead of indexing _scopedSettings by hand. _scopedSettings itself is
12810
- // retained unchanged as the #338-privacy fail-safe path: _hostBehaviors
12811
- // already degrades to FALLBACK_HOST_BEHAVIORS (see that constant's comment
12812
- // above) when the registry fails to load, whereas resolveScope's registry
13129
+ // retained unchanged as the #338-privacy fail-safe path: hostBehaviorsFor
13130
+ // already degrades to its #338 floor (see runtime-name-policy.cts)
13131
+ // when the registry fails to load, whereas resolveScope's registry
12813
13132
  // lookup throws in that same scenario (_installScope is null when it did).
12814
13133
  // Falling back to _scopedSettings[_installScopeId] there — and keeping the
12815
13134
  // non-local-claude branch's expression untouched — means this is
12816
13135
  // byte-identical to the pre-migration computation in every case, including
12817
13136
  // the broken-registry fail-safe floor.
12818
- const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
13137
+ const _scopedSettings = hostBehaviorsFor(runtime).settingsFileByScope || null;
12819
13138
  const _currentScopeSettingsFile = _installScope
12820
13139
  ? _installScope.settingsFile
12821
13140
  : (_scopedSettings ? (_scopedSettings[_installScopeId] ?? null) : null);
@@ -12916,8 +13235,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12916
13235
  persistActiveProfileMarker();
12917
13236
  // Callers index this result by `runtime` (installAllRuntimes' statusline
12918
13237
  // lookup), so every early exit must return the full shape — a bare return
12919
- // crashes the install rather than skipping one file.
12920
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
13238
+ // crashes the install rather than skipping one file. That includes
13239
+ // configuredEntrypoints/rollbackInstallerMigrations: rollbackFinalizedInstallerMigrations
13240
+ // reads result.rollbackInstallerMigrations unconditionally, and an omitted
13241
+ // field there silently skips this runtime's rollback on a finalize-stage failure.
13242
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, configuredEntrypoints: [], rollbackInstallerMigrations };
12921
13243
  }
12922
13244
  const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
12923
13245
  // #3002 CR / #3662: rewrite legacy `node .../gsd-*.js` command strings (pre-
@@ -12938,8 +13260,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12938
13260
  // Descriptor-driven (ADR-1239 / #2096): hookPathStyle comes from the
12939
13261
  // runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
12940
13262
  // check inside projectLocalHookPrefix.
12941
- const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
12942
- const hookOpts = { portableHooks: hasPortableHooks, runtime };
13263
+ const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: hostBehaviorsFor(runtime).hookPathStyle });
13264
+ const settingsEntrypoints = [];
13265
+ const hookOpts = {
13266
+ portableHooks: hasPortableHooks,
13267
+ runtime,
13268
+ configPath: settingsPath,
13269
+ // #4249: track unconditionally. Gating on `plan.hooksSurface ===
13270
+ // 'settings-json'` made tracking depend on an unasserted
13271
+ // installSurface/hooksSurface coupling — a descriptor that broke it would
13272
+ // silently drop this runtime out of validation. Everything recorded here
13273
+ // lands in settings.json by construction, and the registered-command
13274
+ // filter below already discards entries no hook actually references.
13275
+ configuredEntrypoints: settingsEntrypoints,
13276
+ };
12943
13277
  // #2979: local-install hook commands also use a runner GUI/minimal-PATH
12944
13278
  // runtimes can resolve. Bare `node` fails when the host launches the
12945
13279
  // runtime with a stripped PATH (Finder/Antigravity/etc) — #3662 replaces
@@ -12953,19 +13287,19 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12953
13287
  // `node` command that recreates the #2979 failure.
12954
13288
  const localCmd = (hookFile) => localNodeRunner === null
12955
13289
  ? null
12956
- : projectShellCommandText({
13290
+ : hooksSurface.recordConfiguredHookCommand(projectShellCommandText({
12957
13291
  runnerToken: localNodeRunner,
12958
13292
  argTokens: [`${localPrefix}/hooks/${hookFile}`],
12959
13293
  runtime,
12960
13294
  platform: process.platform,
12961
- });
12962
- const localShellCmd = (hookFile) => buildLocalShellHookCommand({
13295
+ }), targetDir, hookFile, hookOpts);
13296
+ const localShellCmd = (hookFile) => hooksSurface.recordConfiguredHookCommand(buildLocalShellHookCommand({
12963
13297
  localPrefix,
12964
13298
  hookFile,
12965
13299
  bashRunner: localBashRunner,
12966
13300
  runtime,
12967
13301
  platform: process.platform,
12968
- });
13302
+ }), targetDir, hookFile, hookOpts);
12969
13303
  const statuslineCommand = isGlobal
12970
13304
  ? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
12971
13305
  : localCmd('gsd-statusline.js');
@@ -13029,12 +13363,37 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13029
13363
  // installAllRuntimes can register it at finalize time when the user opts
13030
13364
  // in (#2795). Computed here (not in finishInstall) so the same buildHookCommand
13031
13365
  // / localCmd resolution logic is shared with the other JS hooks.
13032
- const updateBannerCommand = _hostBehaviors(runtime).skipUpdateBannerCommand
13366
+ const updateBannerCommand = hostBehaviorsFor(runtime).skipUpdateBannerCommand
13033
13367
  ? null
13034
13368
  : (isGlobal
13035
13369
  ? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
13036
13370
  : localCmd('gsd-update-banner.js'));
13037
13371
 
13372
+ const registeredHookCommands = Object.values(settings.hooks || {})
13373
+ .flatMap(groups => Array.isArray(groups) ? groups : [])
13374
+ .flatMap(group => Array.isArray(group && group.hooks) ? group.hooks : [])
13375
+ .map(hook => hook && hook.command)
13376
+ .filter(command => typeof command === 'string');
13377
+ // #4249: match by the managed script's `/hooks/<basename>` path segment, not
13378
+ // by exact command-string equality. The blocking-guard hooks above register
13379
+ // only-if-absent, so a hook already present from a prior install keeps its
13380
+ // OLD command untouched — but `track()` always records the FRESHLY computed
13381
+ // command for it, which never equals what's actually persisted. Matching on
13382
+ // the segment (present in the persisted command either way, since every
13383
+ // entry.scriptPath is <configDir>/hooks/<name> by construction) keeps an
13384
+ // already-registered, still-active hook in the validated set instead of
13385
+ // silently dropping it (#4154 Blocker) — anchored on `/hooks/` rather than a
13386
+ // bare basename so an unrelated user command that merely mentions the same
13387
+ // filename can't false-positive into GSD's validated set.
13388
+ configuredEntrypoints.push(
13389
+ ...settingsEntrypoints.filter(entry => {
13390
+ const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
13391
+ return registeredHookCommands.some(command => command.includes(hooksSegment));
13392
+ }),
13393
+ );
13394
+ const statuslineEntrypoints = settingsEntrypoints.filter(entry => entry.command === statuslineCommand);
13395
+ const updateBannerEntrypoints = settingsEntrypoints.filter(entry => entry.command === updateBannerCommand);
13396
+
13038
13397
  // #683: Set worktree.baseRef:"head" in settings.local.json for local Claude installs.
13039
13398
  // Both fresh and upgrade paths apply only when worktrees are enabled for the project.
13040
13399
  // Never applies to global installs, non-Claude runtimes, or when the user already
@@ -13103,21 +13462,71 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13103
13462
  settings,
13104
13463
  statuslineCommand,
13105
13464
  updateBannerCommand,
13465
+ statuslineEntrypoints,
13466
+ updateBannerEntrypoints,
13106
13467
  runtime,
13107
13468
  configDir: targetDir,
13108
13469
  rollbackInstallerMigrations,
13470
+ configuredEntrypoints,
13109
13471
  };
13110
13472
  }
13111
13473
 
13112
- /**
13113
- * Apply statusline config, then print completion message
13114
- */
13474
+ // #4249 (review, Major): rollback consequence differs by runtime surface —
13475
+ // see docs/how-to/update-gsd.md's rollback-matrix paragraph, which this
13476
+ // mirrors. Codex reverts (pre-install snapshot restore); Cursor/Windsurf/
13477
+ // Kimi/Kimi Code/Cline already wrote their config file inside install(),
13478
+ // ahead of this gate, with no revert path, so it is left on disk broken;
13479
+ // every other (settings.json-based) runtime writes strictly after this gate,
13480
+ // so a failure here means nothing new was persisted for it.
13481
+ const ENTRYPOINT_LEFT_UNREVERTED_RUNTIMES = new Set(['cursor', 'windsurf', 'kimi', 'kimi-code', 'cline']);
13482
+ function describeEntrypointConsequence(invalidRuntime) {
13483
+ if (invalidRuntime === 'codex') return 'reverted: its pre-install snapshot was restored';
13484
+ 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';
13485
+ return 'not persisted: this runtime writes its config after this check';
13486
+ }
13487
+
13488
+ function assertConfiguredEntrypoints(entries) {
13489
+ // #4249: some writers push the same (configPath, scriptPath) pair more than
13490
+ // once (e.g. Kimi's context-monitor hook registered under several events,
13491
+ // or the portable resolver script shared by every portable JS hook) — keep
13492
+ // one so a broken entry is reported once, not once per duplicate.
13493
+ const seen = new Set();
13494
+ const deduped = (entries || []).filter((entry) => {
13495
+ const key = JSON.stringify([entry.configPath, entry.scriptPath]);
13496
+ if (seen.has(key)) return false;
13497
+ seen.add(key);
13498
+ return true;
13499
+ });
13500
+ const validation = hooksSurface.validateConfiguredEntrypoints(deduped);
13501
+ if (validation.ok) return;
13502
+
13503
+ const error = new Error(
13504
+ // #4249: lead each entry with its runtime, and name the actual consequence
13505
+ // for that runtime (review, Major) — the aggregate gate is all-or-nothing
13506
+ // across every runtime being installed, and a failure here can revert a
13507
+ // runtime whose own entrypoints were fine (see
13508
+ // rollbackFinalizedInstallerMigrations) while leaving another runtime's
13509
+ // already-written config broken on disk with no revert at all, so an
13510
+ // operator reading only this message must be able to tell WHOSE
13511
+ // entrypoint broke and WHAT that means for their config, not just that
13512
+ // something did.
13513
+ `Configured entrypoint validation failed: ${validation.invalid.map(({ runtime: invalidRuntime, role, path: invalidPath, reason }) => `${invalidRuntime} ${role} ${invalidPath} (${reason}) [${describeEntrypointConsequence(invalidRuntime)}]`).join(', ')}`,
13514
+ );
13515
+ error.configuredEntrypointValidation = validation;
13516
+ throw error;
13517
+ }
13518
+
13519
+ // #4249: `bannerOpts.configuredEntrypoints` is the ONLY source assertConfiguredEntrypoints
13520
+ // checks below — a caller that omits it (or calls finishInstall directly instead of
13521
+ // through installAllRuntimes) gets zero entrypoint validation, silently. installAllRuntimes
13522
+ // always passes the full set (per-runtime entries plus statusline/updateBanner); any other
13523
+ // caller must do the same for this gate to mean anything.
13115
13524
  function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
13116
13525
  // #2093: isKilo dropped — the Kilo permissions-writer call below is gated
13117
13526
  // on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
13118
13527
  // #2094: isTrae dropped — unused in this function.
13119
13528
  // #2095: isKimi dropped — the Kimi "Done!" banner below reads
13120
- // _hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
13529
+ // hostBehaviorsFor(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
13121
13530
  // #2096: isAntigravity dropped — unused in this function.
13122
13531
  // #2098: isCodebuddy dropped — unused in this function.
13123
13532
  // #2099: isCopilot dropped — unused in this function.
@@ -13125,7 +13534,20 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13125
13534
  const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
13126
13535
  const plan = resolveInstallPlan(runtime);
13127
13536
 
13128
- if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
13537
+ // #4249 Major: validate BEFORE this function's own settings.json write (and
13538
+ // before writeNonClaudeDefaults) instead of after. Cursor/Windsurf/Kimi/Cline
13539
+ // already persisted their config inside install() by this point, with no
13540
+ // rollback path covering those writes; Codex also persists inside install()
13541
+ // but its rollback binds to a full pre-install snapshot restore, so it IS
13542
+ // covered (see docs/how-to/update-gsd.md). For the settings-json surface
13543
+ // this ordering means a failing validation never reaches this function's
13544
+ // own write at all. On the production path this is a redundant backstop —
13545
+ // installAllRuntimes's own aggregate assertConfiguredEntrypoints call
13546
+ // already validates the superset before finishInstall runs for any
13547
+ // runtime — kept for a caller that invokes finishInstall directly.
13548
+ assertConfiguredEntrypoints(bannerOpts.configuredEntrypoints);
13549
+
13550
+ if (shouldInstallStatusline && plan.writesSharedSettings && !hostBehaviorsFor(runtime).skipSettingsUi) {
13129
13551
  if (!isGlobal && !forceStatusline) {
13130
13552
  // Local installs skip statusLine by default: repo settings.json takes precedence over
13131
13553
  // profile-level settings.json in Claude Code, so writing here would silently clobber
@@ -13151,7 +13573,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13151
13573
  // settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
13152
13574
  // cline either lack the surface or use a different config schema.
13153
13575
  const { shouldInstallBanner, bannerCommand } = bannerOpts;
13154
- if (shouldInstallBanner && settings && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
13576
+ if (shouldInstallBanner && settings && plan.writesSharedSettings && !hostBehaviorsFor(runtime).skipSettingsUi) {
13155
13577
  if (!bannerCommand) {
13156
13578
  console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
13157
13579
  } else {
@@ -13180,13 +13602,13 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13180
13602
  // Merges GSD-owned entries non-destructively (preserves existing user permissions).
13181
13603
  // Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
13182
13604
  // settings.json but use different runtimes and do not use these permission strings.
13183
- if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
13605
+ if (hostBehaviorsFor(runtime).permissionsSchema === 'claude') {
13184
13606
  mergeClaudePermissions(settings);
13185
13607
  }
13186
13608
 
13187
13609
  // #2097 UPGRADE 3 (transport:mcp): companion MCP server for runtimes that host
13188
13610
  // MCP in settings.json (Augment). settings.json is golden-excluded, so no golden change.
13189
- if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
13611
+ if (hostBehaviorsFor(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
13190
13612
  mergeGsdMcpServerIntoSettings(settings);
13191
13613
  }
13192
13614
 
@@ -13226,18 +13648,29 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13226
13648
  // generation reads it). This call is idempotent (preserves existing values).
13227
13649
  writeNonClaudeDefaults(runtime);
13228
13650
 
13229
- // program + command are now single-source lookups (ADR-1239 Phase B / #1679):
13230
- // program is the runtime display label; command is the per-host /gsd-new-project
13231
- // invocation syntax.
13651
+ // program is the runtime display label (ADR-1239 Phase B / #1679). The command
13652
+ // is generated from the surface this runtime registered in this install scope
13653
+ // (#5215, ADR-5057 §5 Phase 12) — a runtime that registers no new-project
13654
+ // trigger is told so instead of being sent to a command that does not exist (#4567).
13232
13655
  const program = getRuntimeLabel(runtime);
13233
- const command = getRuntimeNewProjectCommand(runtime);
13656
+ const advertised = resolveAdvertisedNewProject(runtime, isGlobal ? 'global' : 'local');
13657
+ const command = advertised.kind === 'command' ? advertised.command : null;
13658
+ // Host-specific launch/restart steps below stay; only the new-project clause
13659
+ // changes when the runtime registered no such command.
13660
+ const noNewProject = advertised.kind === 'unregistered'
13661
+ ? `${program} registers no new-project command in this scope${advertised.nativeCommand ? ` (its native extension registers ${cyan}${advertised.nativeCommand}${reset})` : ''}.`
13662
+ : '';
13234
13663
 
13235
13664
  // Claude Code global installs use the skills/ format (CC 2.1.88+).
13236
13665
  // Restart is required for CC to pick up newly-installed skills, and the
13237
13666
  // slash-menu surface depends on CC version — so the instruction needs to
13238
13667
  // cover both invocation paths to avoid #2957-style "no commands appear".
13239
- if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
13240
- console.log(`
13668
+ if (hostBehaviorsFor(runtime).skillsGlobalOnboarding && isGlobal) {
13669
+ console.log(command === null ? `
13670
+ ${green}Done!${reset} Restart ${program}. ${noNewProject}
13671
+
13672
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13673
+ ` : `
13241
13674
  ${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}gsd-new-project${reset} skill.
13242
13675
 
13243
13676
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -13245,9 +13678,13 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13245
13678
  return;
13246
13679
  }
13247
13680
 
13248
- if (_hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file') {
13681
+ if (hostBehaviorsFor(runtime).doneBannerStyle === 'kimi-agent-file') {
13249
13682
  const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml';
13250
- console.log(`
13683
+ console.log(command === null ? `
13684
+ ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}. ${noNewProject}
13685
+
13686
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13687
+ ` : `
13251
13688
  ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}.
13252
13689
 
13253
13690
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -13255,7 +13692,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13255
13692
  return;
13256
13693
  }
13257
13694
 
13258
- console.log(`
13695
+ console.log(command === null ? `
13696
+ ${green}Done!${reset} GSD is installed for ${program}, which registers no new-project command in this scope${advertised.nativeCommand ? ` (its native extension registers ${cyan}${advertised.nativeCommand}${reset})` : ''}.
13697
+
13698
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13699
+ ` : `
13259
13700
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
13260
13701
 
13261
13702
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -14022,10 +14463,32 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
14022
14463
 
14023
14464
  const rollbackFinalizedInstallerMigrations = (error) => {
14024
14465
  const rollbackFailures = [];
14466
+ // #4249: this discriminates on the error's KIND, never on which runtime
14467
+ // owns the failing entrypoint. `wide` is true for ANY entrypoint-validation
14468
+ // failure from ANY runtime, by design: the aggregate gate exists so a
14469
+ // multi-runtime install cannot report success while one of its entrypoints
14470
+ // is broken, so an invalid Cline entrypoint reverts Codex's pre-install
14471
+ // snapshot too — even though Codex itself was fine and its own "Done!"
14472
+ // summary already printed. tests/configured-entrypoint-validation.test.cjs
14473
+ // ('an aggregate entrypoint validation failure rolls the Codex install
14474
+ // back') exercises exactly that, and it is the all-or-nothing behaviour
14475
+ // docs/how-to/update-gsd.md documents.
14476
+ //
14477
+ // What this narrows is the OTHER axis: a finalize-stage exception that is
14478
+ // not an entrypoint-validation failure at all — e.g. a sibling runtime's
14479
+ // permission-config write dying with EACCES — gets only the
14480
+ // installer-migrations-only rollback that Phase 4 specifies
14481
+ // (docs/installer-migrations.md#phase-4-installupdate-integration).
14482
+ // Un-installing (and, on update, downgrading) an already-"Done!" Codex over
14483
+ // an unrelated error is not an outcome any doc promises, while the sibling
14484
+ // surfaces that write config inside install() would keep theirs regardless.
14485
+ const wide = !!(error && error.configuredEntrypointValidation);
14025
14486
  for (const result of [...results].reverse()) {
14026
- if (!result || typeof result.rollbackInstallerMigrations !== 'function') continue;
14487
+ if (!result) continue;
14488
+ const rollback = (wide && result.rollbackPreInstallSnapshot) || result.rollbackInstallerMigrations;
14489
+ if (typeof rollback !== 'function') continue;
14027
14490
  try {
14028
- result.rollbackInstallerMigrations();
14491
+ rollback();
14029
14492
  } catch (rollbackError) {
14030
14493
  rollbackFailures.push({
14031
14494
  runtime: result.runtime,
@@ -14053,6 +14516,19 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
14053
14516
 
14054
14517
  const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
14055
14518
  try {
14519
+ const selectedConfiguredEntrypoints = (result) => {
14520
+ if (!result || result.skipped) return [];
14521
+ const useStatusline = statuslineRuntimes.includes(result.runtime)
14522
+ && shouldInstallStatusline
14523
+ && (isGlobal || forceStatusline);
14524
+ return [
14525
+ ...(result.configuredEntrypoints || []),
14526
+ ...(useStatusline ? (result.statuslineEntrypoints || []) : []),
14527
+ ...(shouldInstallBanner ? (result.updateBannerEntrypoints || []) : []),
14528
+ ];
14529
+ };
14530
+ assertConfiguredEntrypoints(results.flatMap(selectedConfiguredEntrypoints));
14531
+
14056
14532
  const printSummaries = () => {
14057
14533
  for (const result of results) {
14058
14534
  if (result && result.skipped) continue;
@@ -14066,7 +14542,11 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
14066
14542
  result.runtime,
14067
14543
  isGlobal,
14068
14544
  result.configDir,
14069
- { shouldInstallBanner: !!shouldInstallBanner, bannerCommand: result.updateBannerCommand }
14545
+ {
14546
+ shouldInstallBanner: !!shouldInstallBanner,
14547
+ bannerCommand: result.updateBannerCommand,
14548
+ configuredEntrypoints: selectedConfiguredEntrypoints(result),
14549
+ }
14070
14550
  );
14071
14551
  }
14072
14552
  };
@@ -14166,9 +14646,6 @@ module.exports = {
14166
14646
  install,
14167
14647
  installAllRuntimes,
14168
14648
  uninstall,
14169
- // #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
14170
- _resolveHostBehaviors,
14171
- FALLBACK_HOST_BEHAVIORS,
14172
14649
  // #3023 — shared hook bundle directory name, descriptor-driven
14173
14650
  SHARED_HOOKS_DIR_DEFAULT,
14174
14651
  resolveSharedHooksDirName,