@opengsd/gsd-core 1.15.0 → 1.17.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 (505) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +336 -6
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-code-fixer.compact.md +1 -1
  7. package/agents/gsd-code-fixer.md +1 -1
  8. package/agents/gsd-code-reviewer.compact.md +5 -3
  9. package/agents/gsd-code-reviewer.md +8 -6
  10. package/agents/gsd-debug-session-manager.compact.md +7 -1
  11. package/agents/gsd-debug-session-manager.md +7 -1
  12. package/agents/gsd-debugger.md +2 -2
  13. package/agents/gsd-eval-auditor.compact.md +1 -1
  14. package/agents/gsd-eval-auditor.md +1 -1
  15. package/agents/gsd-executor.md +4 -4
  16. package/agents/gsd-intel-updater.compact.md +1 -1
  17. package/agents/gsd-intel-updater.md +1 -1
  18. package/agents/gsd-mempalace-curator.md +2 -2
  19. package/agents/gsd-phase-researcher.md +1 -1
  20. package/agents/gsd-plan-checker.md +7 -2
  21. package/agents/gsd-planner.md +3 -3
  22. package/agents/gsd-project-researcher.compact.md +1 -1
  23. package/agents/gsd-project-researcher.md +1 -1
  24. package/agents/gsd-research-synthesizer.compact.md +1 -1
  25. package/agents/gsd-research-synthesizer.md +1 -1
  26. package/agents/gsd-ui-auditor.compact.md +21 -30
  27. package/agents/gsd-ui-auditor.md +42 -51
  28. package/agents/gsd-ui-researcher.compact.md +1 -1
  29. package/agents/gsd-ui-researcher.md +1 -1
  30. package/agents/gsd-verifier.md +31 -9
  31. package/bin/gsd-mcp-server.js +3 -1
  32. package/bin/install.js +487 -290
  33. package/commands/gsd/add-tests.md +6 -1
  34. package/commands/gsd/ai-integration-phase.md +6 -1
  35. package/commands/gsd/audit-fix.md +5 -0
  36. package/commands/gsd/audit-milestone.md +6 -1
  37. package/commands/gsd/autonomous.md +5 -0
  38. package/commands/gsd/capture.md +8 -4
  39. package/commands/gsd/code-review.md +7 -2
  40. package/commands/gsd/complete-milestone.md +4 -0
  41. package/commands/gsd/config.md +7 -3
  42. package/commands/gsd/debug.md +11 -7
  43. package/commands/gsd/discuss-phase.md +7 -3
  44. package/commands/gsd/docs-update.md +12 -7
  45. package/commands/gsd/eval-review.md +6 -1
  46. package/commands/gsd/execute-phase.md +12 -7
  47. package/commands/gsd/extract-learnings.md +5 -0
  48. package/commands/gsd/fast.md +4 -0
  49. package/commands/gsd/forensics.md +5 -1
  50. package/commands/gsd/graphify.md +10 -6
  51. package/commands/gsd/health.md +5 -0
  52. package/commands/gsd/help.md +7 -2
  53. package/commands/gsd/import.md +7 -3
  54. package/commands/gsd/inbox.md +5 -0
  55. package/commands/gsd/ingest-docs.md +5 -1
  56. package/commands/gsd/manager.md +6 -1
  57. package/commands/gsd/map-codebase.md +7 -3
  58. package/commands/gsd/mempalace-capture.md +5 -1
  59. package/commands/gsd/mempalace-recall.md +5 -1
  60. package/commands/gsd/milestone-summary.md +5 -1
  61. package/commands/gsd/mvp-phase.md +8 -3
  62. package/commands/gsd/new-milestone.md +6 -1
  63. package/commands/gsd/new-project.md +5 -0
  64. package/commands/gsd/next.md +6 -1
  65. package/commands/gsd/ns-context.md +4 -0
  66. package/commands/gsd/ns-ideate.md +4 -0
  67. package/commands/gsd/ns-manage.md +4 -0
  68. package/commands/gsd/ns-project.md +4 -0
  69. package/commands/gsd/ns-review.md +4 -0
  70. package/commands/gsd/ns-workflow.md +4 -0
  71. package/commands/gsd/onboard.md +6 -1
  72. package/commands/gsd/pause-work.md +5 -1
  73. package/commands/gsd/phase.md +8 -4
  74. package/commands/gsd/plan-phase.md +6 -1
  75. package/commands/gsd/plan-review-convergence.md +5 -1
  76. package/commands/gsd/pr-branch.md +4 -0
  77. package/commands/gsd/profile-user.md +5 -1
  78. package/commands/gsd/progress.md +6 -1
  79. package/commands/gsd/quick-batch.md +20 -8
  80. package/commands/gsd/quick.md +12 -7
  81. package/commands/gsd/review.md +5 -1
  82. package/commands/gsd/secure-phase.md +6 -1
  83. package/commands/gsd/ship.md +5 -0
  84. package/commands/gsd/sketch.md +7 -2
  85. package/commands/gsd/spec-phase.md +5 -1
  86. package/commands/gsd/spike.md +8 -3
  87. package/commands/gsd/surface.md +5 -1
  88. package/commands/gsd/thread.md +4 -0
  89. package/commands/gsd/ui-phase.md +6 -1
  90. package/commands/gsd/ui-review.md +6 -1
  91. package/commands/gsd/ultraplan-phase.md +5 -1
  92. package/commands/gsd/undo.md +5 -1
  93. package/commands/gsd/update.md +6 -2
  94. package/commands/gsd/validate-phase.md +6 -1
  95. package/commands/gsd/verify-work.md +6 -1
  96. package/commands/gsd/workspace.md +7 -3
  97. package/gsd-core/bin/gsd-tools.cjs +159 -58
  98. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  99. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  100. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  101. package/gsd-core/bin/lib/audit.cjs +78 -44
  102. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  103. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  104. package/gsd-core/bin/lib/capability-registry.cjs +249 -117
  105. package/gsd-core/bin/lib/capability-validator.cjs +18 -1
  106. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  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 +11 -8
  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 +201 -178
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config.cjs +43 -12
  117. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  118. package/gsd-core/bin/lib/coverage.cjs +4 -8
  119. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  120. package/gsd-core/bin/lib/drift.cjs +177 -42
  121. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  122. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  123. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  124. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  125. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  126. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  127. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  128. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  129. package/gsd-core/bin/lib/gate-context-drift.cjs +142 -0
  130. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  131. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  132. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  133. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  134. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  135. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  136. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  137. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  138. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  139. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  140. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  142. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  143. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  144. package/gsd-core/bin/lib/gate-ui-safety.cjs +81 -0
  145. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  146. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  147. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  148. package/gsd-core/bin/lib/graphify.cjs +3 -2
  149. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  150. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  151. package/gsd-core/bin/lib/init.cjs +194 -108
  152. package/gsd-core/bin/lib/install-engine.cjs +30 -35
  153. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  154. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  155. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  156. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  157. package/gsd-core/bin/lib/io.cjs +122 -3
  158. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  159. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  160. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  161. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  162. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  163. package/gsd-core/bin/lib/milestone.cjs +19 -1
  164. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  165. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  166. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  167. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  168. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  169. package/gsd-core/bin/lib/pattern.cjs +10 -0
  170. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  171. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  172. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  173. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  174. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  175. package/gsd-core/bin/lib/phase.cjs +301 -80
  176. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  177. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  178. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  179. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  180. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  181. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  184. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  185. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  186. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +13 -9
  187. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  188. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  189. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  190. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  191. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  192. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  193. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +186 -161
  194. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  195. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  196. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  197. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +365 -594
  198. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  199. package/gsd-core/bin/lib/runtime-slash.cjs +60 -30
  200. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  201. package/gsd-core/bin/lib/smart-entry.cjs +111 -56
  202. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  203. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  204. package/gsd-core/bin/lib/state-document.cjs +100 -22
  205. package/gsd-core/bin/lib/state-transition.cjs +67 -9
  206. package/gsd-core/bin/lib/state.cjs +668 -171
  207. package/gsd-core/bin/lib/surface.cjs +2 -1
  208. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  209. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  210. package/gsd-core/bin/lib/uat.cjs +424 -7
  211. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  212. package/gsd-core/bin/lib/update-context.cjs +18 -1
  213. package/gsd-core/bin/lib/vendor/README.md +31 -9
  214. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  215. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  216. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  217. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  218. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  219. package/gsd-core/bin/lib/verification.cjs +1265 -324
  220. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  221. package/gsd-core/bin/lib/verify.cjs +286 -567
  222. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  223. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  224. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  225. package/gsd-core/bin/lib/workstream.cjs +11 -2
  226. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  227. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  228. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  229. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  230. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  231. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  232. package/gsd-core/references/execute-phase-response-language.md +1 -1
  233. package/gsd-core/references/gsd-run-resolver.md +1 -1
  234. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  235. package/gsd-core/references/offer-next.md +1 -1
  236. package/gsd-core/references/planning-config.md +1 -1
  237. package/gsd-core/references/spidr-splitting.md +1 -1
  238. package/gsd-core/references/tdd.md +10 -4
  239. package/gsd-core/references/verifier-phase-gates.md +5 -2
  240. package/gsd-core/references/verify-mvp-mode.md +2 -2
  241. package/gsd-core/references/workstream-flag.md +33 -3
  242. package/gsd-core/templates/README.md +1 -1
  243. package/gsd-core/templates/UAT.md +17 -1
  244. package/gsd-core/templates/config.json +2 -11
  245. package/gsd-core/templates/verification-report.md +1 -1
  246. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  247. package/gsd-core/workflows/add-backlog.md +1 -1
  248. package/gsd-core/workflows/add-phase.md +8 -7
  249. package/gsd-core/workflows/add-tests.md +3 -2
  250. package/gsd-core/workflows/add-todo.md +4 -3
  251. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  252. package/gsd-core/workflows/audit-fix.md +1 -1
  253. package/gsd-core/workflows/audit-milestone.md +4 -3
  254. package/gsd-core/workflows/audit-uat.md +1 -1
  255. package/gsd-core/workflows/autonomous.md +28 -18
  256. package/gsd-core/workflows/check-todos.md +6 -5
  257. package/gsd-core/workflows/cleanup.md +1 -1
  258. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  259. package/gsd-core/workflows/code-review-fix.md +5 -4
  260. package/gsd-core/workflows/code-review.md +100 -91
  261. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  262. package/gsd-core/workflows/complete-milestone.md +12 -7
  263. package/gsd-core/workflows/debug.md +30 -5
  264. package/gsd-core/workflows/diagnose-issues.md +3 -2
  265. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  266. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  267. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  268. package/gsd-core/workflows/discuss-phase.md +4 -3
  269. package/gsd-core/workflows/do.md +1 -1
  270. package/gsd-core/workflows/docs-update.md +4 -3
  271. package/gsd-core/workflows/edit-phase.md +4 -3
  272. package/gsd-core/workflows/eval-review.md +5 -3
  273. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  274. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  275. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  276. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  277. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +2 -2
  278. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  279. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  280. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  281. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  282. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  283. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  284. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  285. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  286. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  288. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  289. package/gsd-core/workflows/execute-phase.md +81 -171
  290. package/gsd-core/workflows/execute-plan.md +13 -19
  291. package/gsd-core/workflows/explore.md +2 -2
  292. package/gsd-core/workflows/extract-learnings.md +3 -2
  293. package/gsd-core/workflows/fast.md +1 -1
  294. package/gsd-core/workflows/forensics.md +1 -1
  295. package/gsd-core/workflows/graduation.md +1 -1
  296. package/gsd-core/workflows/health.md +2 -1
  297. package/gsd-core/workflows/import.md +3 -2
  298. package/gsd-core/workflows/inbox.md +1 -1
  299. package/gsd-core/workflows/ingest-docs.md +1 -1
  300. package/gsd-core/workflows/insert-phase.md +4 -3
  301. package/gsd-core/workflows/list-seeds.md +1 -1
  302. package/gsd-core/workflows/list-workspaces.md +1 -1
  303. package/gsd-core/workflows/manager.md +5 -3
  304. package/gsd-core/workflows/map-codebase.md +4 -3
  305. package/gsd-core/workflows/milestone-summary.md +3 -2
  306. package/gsd-core/workflows/mvp-phase.md +14 -14
  307. package/gsd-core/workflows/new-milestone.md +10 -10
  308. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  309. package/gsd-core/workflows/new-project.md +1 -1
  310. package/gsd-core/workflows/new-workspace.md +1 -1
  311. package/gsd-core/workflows/next.md +1 -1
  312. package/gsd-core/workflows/pause-work.md +2 -2
  313. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  314. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  315. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  316. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  317. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  318. package/gsd-core/workflows/plan-phase.md +27 -18
  319. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  320. package/gsd-core/workflows/plant-seed.md +1 -1
  321. package/gsd-core/workflows/pr-branch.md +1 -1
  322. package/gsd-core/workflows/profile-user.md +1 -1
  323. package/gsd-core/workflows/progress.md +19 -49
  324. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  325. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  326. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  327. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  328. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  329. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  330. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  331. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  332. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  333. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  334. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  335. package/gsd-core/workflows/quick-batch.md +15 -10
  336. package/gsd-core/workflows/quick.md +42 -31
  337. package/gsd-core/workflows/remove-phase.md +3 -2
  338. package/gsd-core/workflows/remove-workspace.md +1 -1
  339. package/gsd-core/workflows/resume-project.md +17 -12
  340. package/gsd-core/workflows/review.md +3 -2
  341. package/gsd-core/workflows/scan.md +3 -2
  342. package/gsd-core/workflows/secure-phase.md +11 -12
  343. package/gsd-core/workflows/settings-advanced.md +1 -1
  344. package/gsd-core/workflows/settings-integrations.md +1 -1
  345. package/gsd-core/workflows/settings.md +1 -1
  346. package/gsd-core/workflows/ship.md +12 -12
  347. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  348. package/gsd-core/workflows/sketch.md +1 -1
  349. package/gsd-core/workflows/smart-entry.md +1 -1
  350. package/gsd-core/workflows/spec-phase.md +1 -1
  351. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  352. package/gsd-core/workflows/spike.md +1 -1
  353. package/gsd-core/workflows/stats.md +1 -1
  354. package/gsd-core/workflows/sync-skills.md +1 -1
  355. package/gsd-core/workflows/thread.md +2 -2
  356. package/gsd-core/workflows/transition.md +13 -23
  357. package/gsd-core/workflows/ui-phase.md +5 -4
  358. package/gsd-core/workflows/ui-review.md +4 -3
  359. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  360. package/gsd-core/workflows/undo.md +1 -1
  361. package/gsd-core/workflows/validate-phase.md +10 -12
  362. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  365. package/gsd-core/workflows/verify-work.md +68 -59
  366. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  367. package/hooks/dist/gsd-check-update-worker.js +8 -0
  368. package/hooks/dist/gsd-check-update.js +34 -20
  369. package/hooks/dist/gsd-context-monitor.js +45 -13
  370. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  371. package/hooks/dist/gsd-secret-read-guard.js +263 -16
  372. package/hooks/dist/gsd-statusline.js +336 -29
  373. package/hooks/dist/gsd-update-banner.js +8 -0
  374. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  375. package/hooks/dist/gsd-workflow-guard.js +5 -4
  376. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  377. package/hooks/dist/lib/cli-exit.js +12 -0
  378. package/hooks/dist/lib/git-probe.js +17 -1
  379. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  380. package/hooks/gsd-agent-isolation-guard.js +8 -0
  381. package/hooks/gsd-check-update-worker.js +8 -0
  382. package/hooks/gsd-check-update.js +34 -20
  383. package/hooks/gsd-context-monitor.js +45 -13
  384. package/hooks/gsd-cursor-subagent-start.js +8 -0
  385. package/hooks/gsd-secret-read-guard.js +263 -16
  386. package/hooks/gsd-statusline.js +336 -29
  387. package/hooks/gsd-update-banner.js +8 -0
  388. package/hooks/gsd-windsurf-pre-write.js +11 -2
  389. package/hooks/gsd-workflow-guard.js +5 -4
  390. package/hooks/gsd-worktree-path-guard.js +6 -2
  391. package/hooks/hooks.json +5 -5
  392. package/hooks/lib/cli-exit.js +12 -0
  393. package/hooks/lib/git-probe.js +17 -1
  394. package/hooks/lib/isolation-sentinel.js +2 -2
  395. package/package.json +21 -4
  396. package/scripts/changeset/parse.cjs +52 -4
  397. package/scripts/ci-timeout-report.cjs +770 -4
  398. package/scripts/command-contract-helpers.cjs +12 -8
  399. package/scripts/docs-guard-registry.cjs +6 -0
  400. package/scripts/gen-capability-registry.cjs +7 -1
  401. package/scripts/gen-features.cjs +13 -8
  402. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  403. package/scripts/gen-loop-host-contract.cjs +14 -2
  404. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  405. package/scripts/gen-plugin-skills.cjs +87 -1
  406. package/scripts/gen-research-agents.cjs +24 -31
  407. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  408. package/scripts/gen-test-timings.cjs +32 -7
  409. package/scripts/lib/cli-exit.cjs +12 -0
  410. package/scripts/lib/macos-conformance-tier.generated.cjs +24 -2
  411. package/scripts/lib/ndjson-reporter.cjs +28 -3
  412. package/scripts/lib/platform-conformance-tier.generated.cjs +44 -5
  413. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  414. package/scripts/lib/vendor-bundle.cjs +59 -0
  415. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  416. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  417. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  418. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  419. package/scripts/lint-descriptions.cjs +7 -3
  420. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  421. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  422. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  423. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  424. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  425. package/scripts/lint-phase-id-drift.cjs +29 -0
  426. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  427. package/scripts/lint-response-language-coverage.cjs +3 -0
  428. package/scripts/lint-skill-deps.cjs +7 -3
  429. package/scripts/lint-test-file-count.allowlist.json +11 -1
  430. package/scripts/lint-test-file-count.cjs +34 -1
  431. package/scripts/lint-vendored-deps.cjs +41 -5
  432. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  433. package/scripts/mutation-matrix.cjs +53 -6
  434. package/scripts/release-tarball-smoke.cjs +14 -7
  435. package/scripts/require-issue-link-policy.cjs +6 -2
  436. package/scripts/sync-runtime-launcher.cjs +184 -2
  437. package/scripts/verify-npm-publish.cjs +76 -20
  438. package/skills/gsd-add-tests/SKILL.md +6 -1
  439. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  440. package/skills/gsd-audit-fix/SKILL.md +5 -0
  441. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  442. package/skills/gsd-autonomous/SKILL.md +5 -0
  443. package/skills/gsd-capture/SKILL.md +8 -4
  444. package/skills/gsd-code-review/SKILL.md +7 -2
  445. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  446. package/skills/gsd-config/SKILL.md +8 -4
  447. package/skills/gsd-debug/SKILL.md +11 -7
  448. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  449. package/skills/gsd-docs-update/SKILL.md +12 -7
  450. package/skills/gsd-eval-review/SKILL.md +6 -1
  451. package/skills/gsd-execute-phase/SKILL.md +12 -7
  452. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  453. package/skills/gsd-fast/SKILL.md +4 -0
  454. package/skills/gsd-forensics/SKILL.md +5 -1
  455. package/skills/gsd-graphify/SKILL.md +10 -6
  456. package/skills/gsd-health/SKILL.md +5 -0
  457. package/skills/gsd-help/SKILL.md +7 -2
  458. package/skills/gsd-import/SKILL.md +7 -3
  459. package/skills/gsd-inbox/SKILL.md +5 -0
  460. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  461. package/skills/gsd-manager/SKILL.md +6 -1
  462. package/skills/gsd-map-codebase/SKILL.md +7 -3
  463. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  464. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  465. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  466. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  467. package/skills/gsd-new-milestone/SKILL.md +6 -1
  468. package/skills/gsd-new-project/SKILL.md +5 -0
  469. package/skills/gsd-next/SKILL.md +6 -1
  470. package/skills/gsd-ns-context/SKILL.md +4 -0
  471. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  472. package/skills/gsd-ns-manage/SKILL.md +4 -0
  473. package/skills/gsd-ns-project/SKILL.md +4 -0
  474. package/skills/gsd-ns-review/SKILL.md +4 -0
  475. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  476. package/skills/gsd-onboard/SKILL.md +6 -1
  477. package/skills/gsd-pause-work/SKILL.md +5 -1
  478. package/skills/gsd-phase/SKILL.md +8 -4
  479. package/skills/gsd-plan-phase/SKILL.md +6 -1
  480. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  481. package/skills/gsd-pr-branch/SKILL.md +4 -0
  482. package/skills/gsd-profile-user/SKILL.md +5 -1
  483. package/skills/gsd-progress/SKILL.md +6 -1
  484. package/skills/gsd-quick/SKILL.md +16 -10
  485. package/skills/gsd-quick-batch/SKILL.md +20 -8
  486. package/skills/gsd-review/SKILL.md +5 -1
  487. package/skills/gsd-review-backlog/SKILL.md +3 -2
  488. package/skills/gsd-secure-phase/SKILL.md +6 -1
  489. package/skills/gsd-ship/SKILL.md +5 -0
  490. package/skills/gsd-sketch/SKILL.md +7 -2
  491. package/skills/gsd-spec-phase/SKILL.md +5 -1
  492. package/skills/gsd-spike/SKILL.md +8 -3
  493. package/skills/gsd-surface/SKILL.md +5 -1
  494. package/skills/gsd-thread/SKILL.md +4 -0
  495. package/skills/gsd-ui-phase/SKILL.md +6 -1
  496. package/skills/gsd-ui-review/SKILL.md +6 -1
  497. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  498. package/skills/gsd-undo/SKILL.md +5 -1
  499. package/skills/gsd-update/SKILL.md +6 -2
  500. package/skills/gsd-validate-phase/SKILL.md +6 -1
  501. package/skills/gsd-verify-work/SKILL.md +6 -1
  502. package/skills/gsd-workspace/SKILL.md +7 -3
  503. package/skills/gsd-workstreams/SKILL.md +6 -6
  504. package/vscode/package.json +1 -1
  505. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
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
  /**
@@ -190,7 +190,7 @@ function isCodexHooksFeatureKey(key) {
190
190
  // GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
191
191
  //
192
192
  // Format: each string uses Claude Code's documented permission rule syntax \u2014
193
- // "Tool(pattern)" e.g. "Bash(npx gsd-core *)", "Read(.planning/*)"
193
+ // "Tool(pattern)" e.g. "Read(.planning/*)", "Edit(STATE.md)"
194
194
  // "Tool" (bare tool name, no pattern)
195
195
  //
196
196
  // Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
@@ -199,7 +199,6 @@ function isCodexHooksFeatureKey(key) {
199
199
  // instead of scattered literal 'claude' defaults/rosters (#2086).
200
200
  const DEFAULT_RUNTIME = 'claude';
201
201
  const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
202
- 'Bash(npx gsd-core *)',
203
202
  'Read(.planning/*)',
204
203
  'Edit(.planning/*)',
205
204
  'Read(STATE.md)',
@@ -232,9 +231,14 @@ const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
232
231
  // mergeClaudePermissions and uninstall cleanup retire those stale entries on
233
232
  // existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
234
233
  // carries the working `Edit(...)` forms.
234
+ // #5054 — `Bash(npx gsd-core *)` is retired the same way. The project ships
235
+ // as `@opengsd/gsd-core`; the unscoped `gsd-core` npm name belongs to someone
236
+ // else, so the rule pre-authorized whatever that name's owner publishes next.
237
+ // GSD never runs the bare form (update.md uses `--package=@opengsd/gsd-core`).
235
238
  const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
236
239
  'Write(.planning/*)',
237
240
  'Write(STATE.md)',
241
+ 'Bash(npx gsd-core *)',
238
242
  ]);
239
243
 
240
244
  /**
@@ -249,6 +253,7 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
249
253
  * `Write(...)` forms from before the fix) is removed from permissions.allow,
250
254
  * so existing installs end up with the working `Edit(...)` forms instead of
251
255
  * both the dead legacy entry and its replacement sitting side by side.
256
+ * The same filter retires the unscoped `Bash(npx gsd-core *)` rule (#5054).
252
257
  *
253
258
  * Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
254
259
  * are removed from permissions.deny (byte-equal only). Nothing is added to
@@ -408,6 +413,26 @@ const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
408
413
  // two guards require so their copies cannot drift.
409
414
  const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
410
415
 
416
+ /**
417
+ * Every file GSD stages under `hooks/lib/` (#5335). The install copies `hooks/dist/lib/` wholesale (and the
418
+ * allow-listed names from `hooks/lib/`), so a fixed list drifts the moment a helper is added: the new file is
419
+ * staged but never removed, which also keeps `hooks/lib/` non-empty and blocks its rmdir. The set is the
420
+ * allow-list plus the top-level files of the directories the installer copies from, so uninstall and the
421
+ * manifest cannot disagree with install. Only these names are ever removed; a user's own helper beside them
422
+ * is left alone.
423
+ */
424
+ function gsdHookLibFiles() {
425
+ const names = new Set(GSD_HOOK_LIB_FILES);
426
+ for (const parts of [['hooks', 'dist', 'lib'], ['hooks', 'lib']]) {
427
+ try {
428
+ for (const entry of fs.readdirSync(path.join(__dirname, '..', ...parts), { withFileTypes: true })) {
429
+ if (entry.isFile()) names.add(entry.name);
430
+ }
431
+ } catch { /* the directory is absent from this package layout */ }
432
+ }
433
+ return [...names];
434
+ }
435
+
411
436
  /**
412
437
  * Directory name GSD stages its shared hook bundle under, inside a runtime's
413
438
  * install root. Defaults to 'hooks' — the name every runtime used before #3023.
@@ -436,7 +461,7 @@ const GSD_CHANGESET_FILES = [
436
461
  'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
437
462
  'README.md', // documentation only — not user-authored
438
463
  ];
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'];
464
+ 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
465
 
441
466
  // #4544 — the Codex hook payload the install stages into <targetDir>/hooks/.
442
467
  // Hoisted to module scope (the #3184 precedent above) so the rollback's
@@ -473,7 +498,7 @@ const CODEX_HOOKS_TO_COPY = [
473
498
  * @returns {string}
474
499
  */
475
500
  function resolveSharedHooksDirName(runtime) {
476
- const raw = _hostBehaviors(runtime).sharedHooksDirName;
501
+ const raw = hostBehaviorsFor(runtime).sharedHooksDirName;
477
502
  if (typeof raw !== 'string') return SHARED_HOOKS_DIR_DEFAULT;
478
503
  const name = raw.trim();
479
504
  if (name === '') return SHARED_HOOKS_DIR_DEFAULT;
@@ -628,68 +653,6 @@ try {
628
653
  _installedCapabilityRegistry = _capabilityRegistry;
629
654
  }
630
655
 
631
- // Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
632
- // ONLY when the first-party capability registry cannot be loaded (a broken bundle).
633
- // Without it, a registry-load failure would make `_hostBehaviors('claude')` return
634
- // {} and silently route a claude LOCAL install to the repo-shared, committed
635
- // `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
636
- // engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
637
- // hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
638
- // The live descriptor (capabilities/claude/capability.json) remains the source of
639
- // truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
640
- //
641
- // #2870: NOT routed through the Install Scope Module (resolveScope,
642
- // src/install-scope.cts) despite that module owning per-scope settings-file
643
- // resolution elsewhere in this file. resolveScope's own descriptor lookup
644
- // goes through the SAME capability registry require this floor exists to
645
- // survive the failure of (see getRegistry() in install-scope.cts) — so on
646
- // exactly the "registry failed to load" path this constant is for,
647
- // resolveScope would throw too. Routing through it here would trade a
648
- // graceful, documented degrade for a crash in the one case this floor was
649
- // added to prevent. This hardcoded literal is the correct, honest answer,
650
- // not an un-migrated leftover.
651
- const FALLBACK_HOST_BEHAVIORS = Object.freeze({
652
- claude: Object.freeze({
653
- settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
654
- permissionsSchema: 'claude',
655
- sourceMarkerFile: '.gsd-source',
656
- hyphenNameAgentBody: true,
657
- legacyCommandsGsdInstallMigration: true,
658
- legacyCommandsGsdUninstall: 'global',
659
- }),
660
- // antigravity's global config dir is resolved dynamically (env-overridable,
661
- // multi-segment) via resolveAntigravityGlobalDir in getConfigDirFromHome. If the
662
- // registry fails to load, this floor keeps that routing intact instead of
663
- // silently falling through to the generic getGlobalConfigHomeFragment default
664
- // (which would return the wrong '.claude' fragment). (ADR-1239 / #2096)
665
- antigravity: Object.freeze({ globalDirResolver: 'antigravity' }),
666
- });
667
-
668
- /**
669
- * Resolve a runtime's host behaviors from a capability registry, with the
670
- * #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
671
- * is unavailable. Registry is passed in so this is unit-testable under a
672
- * simulated registry-load failure. (ADR-1239 / #2086)
673
- */
674
- function _resolveHostBehaviors(runtime, registry) {
675
- const cap = registry && registry.runtimes && registry.runtimes[runtime];
676
- const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
677
- if (declared) return declared;
678
- return FALLBACK_HOST_BEHAVIORS[runtime] || {};
679
- }
680
-
681
- /**
682
- * Host-specific install behaviors, declared on the runtime descriptor
683
- * (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
684
- * scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
685
- * for runtimes that declare none, so every behavior branch degrades to the
686
- * generic path by default — EXCEPT the reference host's #338-critical keys, which
687
- * fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
688
- */
689
- function _hostBehaviors(runtime) {
690
- return _resolveHostBehaviors(runtime, _capabilityRegistry);
691
- }
692
-
693
656
  /**
694
657
  * Read a runtime's documentation-sourced `hostIntegration.dispatch` axes
695
658
  * (ADR-1239 Phase A — `capabilities/<runtime>/capability.json`
@@ -697,7 +660,7 @@ function _hostBehaviors(runtime) {
697
660
  * background, backgroundDispatch, subagentToolkit}`. These are validated,
698
661
  * closed-vocabulary FACTS about what the runtime's real dispatch primitive
699
662
  * supports (never inferred) — see `docs/reference/host-integration-capability-
700
- * matrix.md` for citations. Unlike `_hostBehaviors` (install *policy*), this is
663
+ * matrix.md` for citations. Unlike `hostBehaviorsFor` (install *policy*), this is
701
664
  * the negotiated *capability* surface; #2284 is its first content-projection
702
665
  * consumer (previously read only by `shouldFlattenDispatch`). Returns `{}` if
703
666
  * the registry or the runtime's descriptor is unavailable, so callers must
@@ -824,8 +787,11 @@ const {
824
787
  discoverInstallerMigrations,
825
788
  MANIFEST_SCHEMA_VERSION,
826
789
  readInstallManifest,
790
+ recordHostGeneration,
827
791
  runInstallerMigrations,
828
792
  } = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
793
+ const { setJsoncValue, removeJsoncValue, hasJsoncFeatures, parseJsonc } = require(path.join(_gsdLibDir, 'jsonc-edit.cjs'));
794
+ const { isGsdMcpEntry, GSD_MCP_NO_PROMPTS_FLAG } = require(path.join(_gsdLibDir, 'opencode-mcp-entry.cjs'));
829
795
  const {
830
796
  assertInstallerMigrationsUnblocked,
831
797
  resolveInstallerMigrationPromptsForNonTty,
@@ -833,6 +799,7 @@ const {
833
799
  } = require(path.join(_gsdLibDir, 'installer-migration-report.cjs'));
834
800
  const {
835
801
  resolveRuntimeArtifactLayout,
802
+ resolveAdvertisedNewProject,
836
803
  } = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
837
804
  const {
838
805
  readSurface,
@@ -1178,13 +1145,13 @@ function getConfigDirFromHome(runtime, isGlobal) {
1178
1145
  // !isGlobal returns at the top of this function.)
1179
1146
  // Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
1180
1147
  // `runtime === 'antigravity'` literal into a read of the runtime's
1181
- // `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which
1182
- // also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is
1148
+ // `hostBehaviors.globalDirResolver` descriptor field (via hostBehaviorsFor, which
1149
+ // also degrades to its #338 floor on registry-load failure). This is
1183
1150
  // antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which
1184
1151
  // windsurf also declares — see capabilities/windsurf/capability.json — and
1185
1152
  // would wrongly route windsurf's global dir through
1186
1153
  // resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity.
1187
- if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') {
1154
+ if (hostBehaviorsFor(runtime).globalDirResolver === 'antigravity') {
1188
1155
  const antigravityDir = resolveAntigravityGlobalDir();
1189
1156
  const rel = path.relative(os.homedir(), antigravityDir);
1190
1157
  const segments = rel.split(path.sep).filter(Boolean);
@@ -1317,6 +1284,17 @@ const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
1317
1284
  // writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
1318
1285
  // surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
1319
1286
  const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
1287
+
1288
+ // #4840: false when the runtime declares `context-warnings` unsupported in its
1289
+ // descriptor (`hostBehaviors.unsupportedFeatures`). gsd-context-monitor.js reads
1290
+ // the metrics bridge file only the statusline hook writes, and the statusline
1291
+ // is wired for Claude Code alone, so on any other host the hook fires on every
1292
+ // tool event, finds no file and exits silently — forever. Such a host gets no
1293
+ // monitor registration (stale ones are stripped), as Codex does since #2586.
1294
+ function runtimeSupportsContextWarnings(runtime) {
1295
+ const unsupported = hostBehaviorsFor(runtime).unsupportedFeatures;
1296
+ return !(Array.isArray(unsupported) && unsupported.includes('context-warnings'));
1297
+ }
1320
1298
  const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
1321
1299
  // processAttribution: pure Co-Authored-By content transform, relocated to the
1322
1300
  // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
@@ -1594,12 +1572,12 @@ function getCommitAttribution(runtime) {
1594
1572
 
1595
1573
  let result;
1596
1574
 
1597
- const _attrResolverKey = _hostBehaviors(runtime).attributionConfigResolver;
1575
+ const _attrResolverKey = hostBehaviorsFor(runtime).attributionConfigResolver;
1598
1576
  if (_attrResolverKey && ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey]) {
1599
1577
  const resolveConfigPath = ATTRIBUTION_CONFIG_RESOLVERS[_attrResolverKey];
1600
1578
  const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
1601
1579
  result = (config && config.disable_ai_attribution === true) ? null : undefined;
1602
- } else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
1580
+ } else if (hostBehaviorsFor(runtime).attributionSource === 'settings-json-commit') {
1603
1581
  // Claude Code
1604
1582
  const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
1605
1583
  if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
@@ -2084,13 +2062,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
2084
2062
  // Hermes' SKILL.md spec lists `version` as a required frontmatter field.
2085
2063
  // Track GSD's package version so Hermes' skill_view() reports a stable
2086
2064
  // identifier per install.
2087
- if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
2065
+ if (hostBehaviorsFor(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
2088
2066
  // #778 (b) — numeric priority for /skills ordering, declared on the runtime
2089
2067
  // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
2090
2068
  // runtimes that declare the flag so Claude/Hermes skill frontmatter is
2091
2069
  // unchanged (they ignore the field, but we keep their output byte-stable).
2092
2070
  // skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2086)
2093
- if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
2071
+ if (hostBehaviorsFor(runtime).skillPriorityFrontmatter) {
2094
2072
  const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
2095
2073
  ? skillName.slice(4)
2096
2074
  : skillName;
@@ -2555,20 +2533,11 @@ function yamlIdentifier(value) {
2555
2533
  return yamlQuote(text);
2556
2534
  }
2557
2535
 
2536
+ // The frontmatter block is the one the one fence owner (`locateFrontmatterFence`) finds, read
2537
+ // through the conversion module's reader — never a local `indexOf('---', 3)` scan, which ended
2538
+ // the block at the first `---` anywhere, even inside a value (found while implementing #5105).
2558
2539
  function extractFrontmatterAndBody(content) {
2559
- if (!content.startsWith('---')) {
2560
- return { frontmatter: null, body: content };
2561
- }
2562
-
2563
- const endIndex = content.indexOf('---', 3);
2564
- if (endIndex === -1) {
2565
- return { frontmatter: null, body: content };
2566
- }
2567
-
2568
- return {
2569
- frontmatter: content.substring(3, endIndex).trim(),
2570
- body: content.substring(endIndex + 3),
2571
- };
2540
+ return runtimeArtifactConversion.extractFrontmatterAndBody(content);
2572
2541
  }
2573
2542
 
2574
2543
  function extractFrontmatterField(frontmatter, fieldName) {
@@ -2813,7 +2782,7 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
2813
2782
  // is not formally documented (thin SPA docs) — descriptor-driven, single
2814
2783
  // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
2815
2784
  // best-effort.
2816
- const soloStage = _hostBehaviors('trae').soloStageMetadata;
2785
+ const soloStage = hostBehaviorsFor('trae').soloStageMetadata;
2817
2786
  if (soloStage) fm += `stage: ${soloStage}\n`;
2818
2787
  fm += '---';
2819
2788
  return `${fm}\n${body}`;
@@ -3806,7 +3775,7 @@ const HERMES_DISPATCH_TOOL_CONFIG = Object.freeze({
3806
3775
  */
3807
3776
  function convertClaudeToHermesMarkdown(content, ctx) {
3808
3777
  const runtime = (ctx && ctx.runtime) || 'hermes';
3809
- const b = _hostBehaviors(runtime).brandingRewrites;
3778
+ const b = hostBehaviorsFor(runtime).brandingRewrites;
3810
3779
  let converted = content;
3811
3780
  if (b) {
3812
3781
  converted = converted.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
@@ -7029,6 +6998,82 @@ function writeCopilotHookConfig(targetDir) {
7029
6998
  * Reads agent .md files from source, extracts metadata, writes .toml configs.
7030
6999
  */
7031
7000
 
7001
+ // #5335: ~/.gsd/defaults.json is machine-wide, so uninstalling one runtime must not strip a default another
7002
+ // still needs, nor one the user chose. A sidecar records which keys an install SET (not ones it found), the
7003
+ // value it set, and which non-Claude runtimes have installed since; uninstall reverts a key only when its
7004
+ // last such runtime is gone and the value is still the one the installer wrote.
7005
+ const DEFAULTS_PROVENANCE_NAME = 'defaults-provenance.json';
7006
+
7007
+ function readDefaultsProvenance(gsdDir) {
7008
+ try {
7009
+ const p = JSON.parse(fs.readFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), 'utf8'));
7010
+ if (p && typeof p === 'object' && p.keys && typeof p.keys === 'object') return p;
7011
+ } catch { /* absent or unreadable */ }
7012
+ return { version: 1, keys: {} };
7013
+ }
7014
+
7015
+ function recordDefaultsProvenance(gsdDir, runtime, defaults, setNow) {
7016
+ const prov = readDefaultsProvenance(gsdDir);
7017
+ let changed = false;
7018
+ for (const key of ['resolve_model_ids', 'runtime']) {
7019
+ let entry = prov.keys[key];
7020
+ if (setNow[key]) {
7021
+ entry = { setValue: defaults[key], runtimes: [] };
7022
+ prov.keys[key] = entry;
7023
+ changed = true;
7024
+ }
7025
+ if (entry && Array.isArray(entry.runtimes) && !entry.runtimes.includes(runtime)) {
7026
+ entry.runtimes.push(runtime);
7027
+ changed = true;
7028
+ }
7029
+ }
7030
+ if (changed) atomicWriteFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), JSON.stringify(prov, null, 2) + '\n', 'utf8');
7031
+ }
7032
+
7033
+ /** @returns {boolean} true when it removed or reverted something */
7034
+ function revertNonClaudeDefaults(runtime) {
7035
+ if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return false;
7036
+ const gsdDir = path.join(os.homedir(), '.gsd');
7037
+ const provPath = path.join(gsdDir, DEFAULTS_PROVENANCE_NAME);
7038
+ if (!fs.existsSync(provPath)) return false;
7039
+ let releaseLock = null;
7040
+ try {
7041
+ releaseLock = acquireInstallMigrationLock(gsdDir);
7042
+ const prov = readDefaultsProvenance(gsdDir);
7043
+ const defaultsPath = path.join(gsdDir, 'defaults.json');
7044
+ let defaults = null;
7045
+ try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* absent or corrupt */ }
7046
+ const reverted = [];
7047
+ for (const [key, entry] of Object.entries(prov.keys)) {
7048
+ if (!entry || !Array.isArray(entry.runtimes)) { delete prov.keys[key]; continue; }
7049
+ entry.runtimes = entry.runtimes.filter((r) => r !== runtime);
7050
+ if (entry.runtimes.length > 0) continue;
7051
+ if (defaults && typeof defaults === 'object' && defaults[key] === entry.setValue) {
7052
+ delete defaults[key];
7053
+ reverted.push(key);
7054
+ }
7055
+ delete prov.keys[key];
7056
+ }
7057
+ if (reverted.length > 0 && defaults) {
7058
+ if (Object.keys(defaults).length === 0) fs.rmSync(defaultsPath, { force: true });
7059
+ else atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
7060
+ console.log(` ${green}✓${reset} Reverted ${reverted.join(', ')} in ~/.gsd/defaults.json (set by the ${runtime} install)`);
7061
+ }
7062
+ if (Object.keys(prov.keys).length === 0) fs.rmSync(provPath, { force: true });
7063
+ else atomicWriteFileSync(provPath, JSON.stringify(prov, null, 2) + '\n', 'utf8');
7064
+ return reverted.length > 0;
7065
+ } catch (e) {
7066
+ console.log(` ${yellow}⚠${reset} Could not update ~/.gsd/defaults.json: ${e.message}`);
7067
+ return false;
7068
+ } finally {
7069
+ if (releaseLock) {
7070
+ try { releaseLock(); } catch { /* the stale-lock reaper clears it */ }
7071
+ }
7072
+ // ~/.gsd is GSD's own directory; drop it once the lock is released and nothing else lives in it.
7073
+ try { fs.rmdirSync(gsdDir); } catch { /* other GSD state lives here */ }
7074
+ }
7075
+ }
7076
+
7032
7077
  /**
7033
7078
  * #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
7034
7079
  * resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
@@ -7040,7 +7085,7 @@ function writeCopilotHookConfig(targetDir) {
7040
7085
  * model aliases). Preserves an explicit `true` opt-in and existing values.
7041
7086
  */
7042
7087
  function writeNonClaudeDefaults(runtime) {
7043
- if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
7088
+ if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
7044
7089
  const gsdDir = path.join(os.homedir(), '.gsd');
7045
7090
  const defaultsPath = path.join(gsdDir, 'defaults.json');
7046
7091
  let releaseLock = null;
@@ -7075,6 +7120,10 @@ function writeNonClaudeDefaults(runtime) {
7075
7120
  atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
7076
7121
  for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
7077
7122
  }
7123
+ recordDefaultsProvenance(gsdDir, runtime, defaults, {
7124
+ resolve_model_ids: shouldDefaultToOmit,
7125
+ runtime: applied.some((m) => m.startsWith('Set runtime:')),
7126
+ });
7078
7127
  } catch (e) {
7079
7128
  console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
7080
7129
  } finally {
@@ -7271,26 +7320,22 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7271
7320
  // Runtime-neutral agent name replacement (#766)
7272
7321
  convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
7273
7322
 
7274
- // Check if content has frontmatter
7275
- if (!convertedContent.startsWith('---')) {
7276
- return convertedContent;
7277
- }
7278
-
7279
- // Find the end of frontmatter
7280
- const endIndex = convertedContent.indexOf('---', 3);
7281
- if (endIndex === -1) {
7323
+ // The frontmatter block, as the one fence owner finds it (none → nothing to convert).
7324
+ const { frontmatter, body } = extractFrontmatterAndBody(convertedContent);
7325
+ if (frontmatter === null) {
7282
7326
  return convertedContent;
7283
7327
  }
7284
7328
 
7285
- const frontmatter = convertedContent.substring(3, endIndex).trim();
7286
- const body = convertedContent.substring(endIndex + 3);
7287
-
7288
7329
  // Parse frontmatter line by line (simple YAML parsing)
7289
7330
  const lines = frontmatter.split('\n');
7290
7331
  const newLines = [];
7291
7332
  let inAllowedTools = false;
7292
7333
  let inSkippedArray = false;
7334
+ let inAgentTools = false;
7335
+ let inDisallowedTools = false;
7293
7336
  const allowedTools = [];
7337
+ const agentTools = [];
7338
+ const agentDisallowed = [];
7294
7339
 
7295
7340
  for (const line of lines) {
7296
7341
  const trimmed = line.trim();
@@ -7306,11 +7351,31 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7306
7351
  continue;
7307
7352
  }
7308
7353
 
7354
+ // Agents: collect the `tools:` allowlist and `disallowedTools:` denylist (block-list continuation
7355
+ // lines) so they can be rendered as `permission:` entries below (mirrors
7356
+ // src/runtime-artifact-conversion.cts's convertClaudeToOpencodeFrontmatter, #5338).
7357
+ if (isAgent && (inAgentTools || inDisallowedTools)) {
7358
+ if (trimmed.startsWith('- ')) {
7359
+ const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2));
7360
+ if (tool !== null) (inAgentTools ? agentTools : agentDisallowed).push(tool);
7361
+ continue;
7362
+ }
7363
+ if (trimmed && !trimmed.startsWith('-')) {
7364
+ inAgentTools = false;
7365
+ inDisallowedTools = false;
7366
+ }
7367
+ }
7368
+
7309
7369
  // Detect inline tools: field (comma-separated string)
7310
7370
  if (trimmed.startsWith('tools:')) {
7311
7371
  if (isAgent) {
7312
- // Agents: strip tools entirely (not supported in OpenCode agent frontmatter)
7313
- inSkippedArray = true;
7372
+ const toolsValue = trimmed.substring(6).trim();
7373
+ // A comment-only value (`tools: # note`) is not inline content: read the block list instead.
7374
+ if (toolsValue && !toolsValue.startsWith('#')) {
7375
+ agentTools.push(...runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
7376
+ } else {
7377
+ inAgentTools = true;
7378
+ }
7314
7379
  continue;
7315
7380
  }
7316
7381
  const toolsValue = trimmed.substring(6).trim();
@@ -7322,8 +7387,18 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7322
7387
  continue;
7323
7388
  }
7324
7389
 
7325
- // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools:
7326
- if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) {
7390
+ if (isAgent && trimmed.startsWith('disallowedTools:')) {
7391
+ const value = trimmed.substring(16).trim();
7392
+ if (value && !value.startsWith('#')) {
7393
+ agentDisallowed.push(...runtimeArtifactConversion._splitToolScalars(value).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
7394
+ } else {
7395
+ inDisallowedTools = true;
7396
+ }
7397
+ continue;
7398
+ }
7399
+
7400
+ // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:
7401
+ if (isAgent && /^(skills|color|memory|maxTurns|permissionMode):/.test(trimmed)) {
7327
7402
  inSkippedArray = true;
7328
7403
  continue;
7329
7404
  }
@@ -7396,6 +7471,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
7396
7471
  if (modelOverride) {
7397
7472
  newLines.push(`model: ${modelOverride}`);
7398
7473
  }
7474
+ newLines.push(...runtimeArtifactConversion._buildOpencodeAgentPermissionBlock(agentTools, agentDisallowed));
7399
7475
  }
7400
7476
 
7401
7477
  // For commands: add tools object if we had allowed-tools or tools
@@ -7438,20 +7514,12 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
7438
7514
  // Runtime-neutral agent name replacement (#766)
7439
7515
  convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md');
7440
7516
 
7441
- // Check if content has frontmatter
7442
- if (!convertedContent.startsWith('---')) {
7443
- return convertedContent;
7444
- }
7445
-
7446
- // Find the end of frontmatter
7447
- const endIndex = convertedContent.indexOf('---', 3);
7448
- if (endIndex === -1) {
7517
+ // The frontmatter block, as the one fence owner finds it (none → nothing to convert).
7518
+ const { frontmatter, body } = extractFrontmatterAndBody(convertedContent);
7519
+ if (frontmatter === null) {
7449
7520
  return convertedContent;
7450
7521
  }
7451
7522
 
7452
- const frontmatter = convertedContent.substring(3, endIndex).trim();
7453
- const body = convertedContent.substring(endIndex + 3);
7454
-
7455
7523
  // Parse frontmatter line by line (simple YAML parsing)
7456
7524
  const lines = frontmatter.split('\n');
7457
7525
  const newLines = [];
@@ -7825,7 +7893,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7825
7893
  },
7826
7894
  },
7827
7895
  // qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via
7828
- // _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering
7896
+ // hostBehaviorsFor(ctx.runtime).brandingRewrites — EXACT regexes/ordering
7829
7897
  // preserved from the prior hardcoded-literal versions (including the
7830
7898
  // qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose
7831
7899
  // target is derived as `${b['.claude/']}skills/`).
@@ -7833,7 +7901,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7833
7901
  md: (content, ctx) => {
7834
7902
  // Guarded (post-review #2092): degrade closed to a no-op if the
7835
7903
  // registry fails to load, instead of throwing on `b['CLAUDE.md']`.
7836
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7904
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7837
7905
  if (b) {
7838
7906
  content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
7839
7907
  // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
@@ -7843,7 +7911,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7843
7911
  return content;
7844
7912
  },
7845
7913
  js: (content, ctx) => {
7846
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7914
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7847
7915
  if (b) {
7848
7916
  content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
7849
7917
  content = content.replace(/\.claude\//g, b['.claude/']);
@@ -7862,7 +7930,7 @@ const RUNTIME_CONTENT_DISPATCH = {
7862
7930
  // hostIntegration.dispatch facts.
7863
7931
  md: (content, ctx) => convertClaudeToHermesMarkdown(content, ctx),
7864
7932
  js: (content, ctx) => {
7865
- const b = _hostBehaviors(ctx.runtime).brandingRewrites;
7933
+ const b = hostBehaviorsFor(ctx.runtime).brandingRewrites;
7866
7934
  if (b) {
7867
7935
  content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
7868
7936
  content = content.replace(/\.claude\//g, b['.claude/']);
@@ -8025,7 +8093,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8025
8093
  // emit path, which never had it: every @~/.claude/gsd-core/… include
8026
8094
  // in a global install's workflows/references tree silently resolved
8027
8095
  // to nothing (54 includes across 22 files on a live install).
8028
- if (runtime === 'claude') {
8096
+ if (hostBehaviorsFor(runtime).restoreAtRefTildeInSpecTree) {
8029
8097
  content = runtimeArtifactConversion._restoreClaudeGlobalAtRefTilde(content, pathPrefix);
8030
8098
  }
8031
8099
  return content;
@@ -8041,7 +8109,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8041
8109
  // copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
8042
8110
  // _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
8043
8111
  // live in real installs (it is a no-op for files without those lines).
8044
- if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
8112
+ if (!hostBehaviorsFor(runtime).authorsCanonicalWorkflow) {
8045
8113
  content = _stampNonClaudeRuntimeDefaults(content, runtime);
8046
8114
  }
8047
8115
 
@@ -8325,7 +8393,7 @@ function reclaimKimiHooksRoot(kimiHooksRoot) {
8325
8393
  const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
8326
8394
  if (fs.existsSync(kimiHooksLibDir)) {
8327
8395
  let removedKimiLibFiles = 0;
8328
- for (const file of GSD_HOOK_LIB_FILES) {
8396
+ for (const file of gsdHookLibFiles()) {
8329
8397
  try {
8330
8398
  fs.unlinkSync(path.join(kimiHooksLibDir, file));
8331
8399
  removedKimiLibFiles++;
@@ -8391,7 +8459,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8391
8459
  // `runtime === 'cline'` branch into hostBehaviors.localTargetIsProjectRoot.
8392
8460
  const targetDir = isGlobal
8393
8461
  ? getGlobalConfigDir(runtime, explicitConfigDir)
8394
- : _hostBehaviors(runtime).localTargetIsProjectRoot
8462
+ : hostBehaviorsFor(runtime).localTargetIsProjectRoot
8395
8463
  ? process.cwd()
8396
8464
  : path.join(process.cwd(), dirName);
8397
8465
 
@@ -8499,7 +8567,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8499
8567
  }
8500
8568
 
8501
8569
  // 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
8502
- if (_hostBehaviors(runtime).tomlConfigInstall) {
8570
+ if (hostBehaviorsFor(runtime).tomlConfigInstall) {
8503
8571
  const codexAgentsDir = path.join(targetDir, 'agents');
8504
8572
  if (fs.existsSync(codexAgentsDir)) {
8505
8573
  const tomlFiles = fs.readdirSync(codexAgentsDir);
@@ -8613,7 +8681,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8613
8681
  // global cross-tool ~/.agents/AGENTS.md target.
8614
8682
  // Descriptor-driven (ADR-1239 / #2090): folded from `runtime === 'cline'`
8615
8683
  // into hostBehaviors.clineRulesSurface.
8616
- if (_hostBehaviors(runtime).clineRulesSurface) {
8684
+ if (hostBehaviorsFor(runtime).clineRulesSurface) {
8617
8685
  const clinerulesDir = path.join(targetDir, '.clinerules');
8618
8686
  for (const rel of ['gsd.md', path.join('hooks', 'PreToolUse')]) {
8619
8687
  const p = path.join(clinerulesDir, rel);
@@ -8663,7 +8731,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8663
8731
  // GSD-managed hook entries from hooks.json and clean up the managed hook
8664
8732
  // scripts. Gated by the hostBehaviors.hooksJsonSurface descriptor axis, not a
8665
8733
  // hardcoded `isCursor` branch.
8666
- if (_hostBehaviors(runtime).hooksJsonSurface) {
8734
+ if (hostBehaviorsFor(runtime).hooksJsonSurface) {
8667
8735
  const hooksJsonCleanup = removeCursorHooksJson(targetDir);
8668
8736
  if (hooksJsonCleanup.changed) {
8669
8737
  removedCount++;
@@ -8723,7 +8791,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8723
8791
 
8724
8792
  // 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
8725
8793
  // #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
8726
- if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
8794
+ if (!isGlobal && hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat') {
8727
8795
  const commandsDir = path.join(targetDir, 'commands');
8728
8796
  // Remove flat gsd-*.md files (current layout after #1367 fix)
8729
8797
  if (fs.existsSync(commandsDir)) {
@@ -8789,7 +8857,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8789
8857
  // removes the directory; we must preserve/restore user artifacts before that path.
8790
8858
  // This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
8791
8859
  // was already removed and skip if so (idempotent).
8792
- if (_hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
8860
+ if (hostBehaviorsFor(runtime).legacyCommandsGsdCleanup === true) {
8793
8861
  // dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
8794
8862
  // that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
8795
8863
  // attempt migration. In practice _runLegacyUninstallCleanup removes it first,
@@ -8961,7 +9029,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8961
9029
  const hooksLibDir = path.join(hooksDir, 'lib');
8962
9030
  if (fs.existsSync(hooksLibDir)) {
8963
9031
  let removedLibFiles = 0;
8964
- for (const file of GSD_HOOK_LIB_FILES) {
9032
+ for (const file of gsdHookLibFiles()) {
8965
9033
  const filePath = path.join(hooksLibDir, file);
8966
9034
  try {
8967
9035
  fs.unlinkSync(filePath);
@@ -8997,6 +9065,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
8997
9065
  console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
8998
9066
  }
8999
9067
  } catch { /* best-effort */ }
9068
+ // Once GSD's hooks, helpers and marker are gone, an empty hooks dir is GSD's own leftover (#5335).
9069
+ // rmdir fails on a non-empty dir, so a user's own hook scripts keep it in place.
9070
+ try { fs.rmdirSync(hooksDir); } catch { /* not empty or absent */ }
9000
9071
  }
9001
9072
 
9002
9073
  // 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
@@ -9004,7 +9075,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9004
9075
  // that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only
9005
9076
  // GSD's own plugin file is removed; the plugins/ dir is pruned only if it
9006
9077
  // becomes empty, preserving any user-authored plugins for that host.
9007
- const _np = _hostBehaviors(runtime).nativePlugin;
9078
+ const _np = hostBehaviorsFor(runtime).nativePlugin;
9008
9079
  if (_np) {
9009
9080
  const pluginsDir = path.join(targetDir, _np.dir);
9010
9081
  const pluginPath = path.join(pluginsDir, _np.file);
@@ -9030,6 +9101,20 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9030
9101
  // unlink above failed, pulling the marker out from under a still-present
9031
9102
  // CommonJS adapter would leave it unloadable. The exact content match
9032
9103
  // still leaves any user-authored package.json in place.
9104
+ // The terminal-client plugin directory (OpenCode 2.x statusline): GSD's file, then its now-empty directory.
9105
+ const _cliU = hostBehaviorsFor(runtime).nativeCliPlugin;
9106
+ if (_cliU) {
9107
+ const cliFile = path.join(targetDir, _cliU.dir, _cliU.file);
9108
+ if (fs.existsSync(cliFile)) {
9109
+ try {
9110
+ fs.unlinkSync(cliFile);
9111
+ removedCount++;
9112
+ removedFromPluginsDir = true;
9113
+ console.log(` ${green}✓${reset} Removed terminal-client plugin (${runtime})`);
9114
+ } catch (_) { /* best-effort */ }
9115
+ }
9116
+ try { fs.rmdirSync(path.join(targetDir, _cliU.dir)); } catch (_) { /* not empty or absent */ }
9117
+ }
9033
9118
  if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
9034
9119
  removedCount++;
9035
9120
  removedFromPluginsDir = true;
@@ -9169,7 +9254,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9169
9254
  // to preserve any user-added allow/deny entries.
9170
9255
  // Uses a local flag to avoid the shared `settingsModified` producing a false
9171
9256
  // "Removed GSD permissions" message when only hooks/statusline changed.
9172
- if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
9257
+ if (hostBehaviorsFor(runtime).permissionsSchema === 'claude' && settings.permissions) {
9173
9258
  let permissionsModified = false;
9174
9259
  if (Array.isArray(settings.permissions.allow)) {
9175
9260
  const before = settings.permissions.allow.length;
@@ -9240,7 +9325,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9240
9325
  // runtimes that host MCP there (Augment), symmetric to the mcp_config.json
9241
9326
  // removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is
9242
9327
  // removed — any other user-configured MCP servers are preserved.
9243
- if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' &&
9328
+ if (hostBehaviorsFor(runtime).mcpCompanion === 'settings-json' &&
9244
9329
  settings.mcpServers && typeof settings.mcpServers === 'object' &&
9245
9330
  settings.mcpServers.gsd !== undefined) {
9246
9331
  delete settings.mcpServers.gsd;
@@ -9257,40 +9342,56 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9257
9342
  }
9258
9343
  }
9259
9344
 
9260
- // 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
9345
+ // 6. For OpenCode, clean up permissions and the companion MCP entry from opencode.json or opencode.jsonc
9261
9346
  if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
9262
9347
  const configPath = resolveOpencodeConfigPath(targetDir);
9263
9348
  if (fs.existsSync(configPath)) {
9264
9349
  try {
9265
- const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
9266
- let modified = false;
9350
+ const originalText = fs.readFileSync(configPath, 'utf8');
9351
+ const config = parseJsonc(originalText);
9352
+ const edits = [];
9267
9353
 
9268
9354
  // Remove GSD permission entries
9269
- if (config.permission) {
9355
+ if (config.permission && typeof config.permission === 'object') {
9270
9356
  for (const permType of ['read', 'external_directory']) {
9271
- if (config.permission[permType]) {
9272
- const keys = Object.keys(config.permission[permType]);
9273
- for (const key of keys) {
9274
- if (key.includes('gsd-core')) {
9275
- delete config.permission[permType][key];
9276
- modified = true;
9277
- }
9278
- }
9279
- // Clean up empty objects
9280
- if (Object.keys(config.permission[permType]).length === 0) {
9281
- delete config.permission[permType];
9357
+ const section = config.permission[permType];
9358
+ if (!section || typeof section !== 'object') continue;
9359
+ for (const key of Object.keys(section)) {
9360
+ if (key.includes('gsd-core')) {
9361
+ delete section[key];
9362
+ edits.push({ path: ['permission', permType, key], remove: true });
9282
9363
  }
9283
9364
  }
9365
+ // Clean up empty objects
9366
+ if (Object.keys(section).length === 0) delete config.permission[permType];
9284
9367
  }
9285
- if (Object.keys(config.permission).length === 0) {
9286
- delete config.permission;
9368
+ if (Object.keys(config.permission).length === 0) delete config.permission;
9369
+ }
9370
+
9371
+ // #5333: remove the companion MCP entry, but only while it is still the entry GSD wrote — a
9372
+ // user-edited or user-defined `gsd` server stays. Both OpenCode generations' shapes: the 1.x
9373
+ // `mcp.gsd` and the 2.x native `mcp.servers.gsd`.
9374
+ let mcpRemoved = false;
9375
+ if (config.mcp && typeof config.mcp === 'object') {
9376
+ if (isGsdMcpEntry(config.mcp.gsd)) {
9377
+ delete config.mcp.gsd;
9378
+ edits.push({ path: ['mcp', 'gsd'], remove: true });
9379
+ mcpRemoved = true;
9380
+ }
9381
+ const servers = config.mcp.servers;
9382
+ if (servers && typeof servers === 'object' && isGsdMcpEntry(servers.gsd)) {
9383
+ delete servers.gsd;
9384
+ edits.push({ path: ['mcp', 'servers', 'gsd'], remove: true });
9385
+ if (Object.keys(servers).length === 0) delete config.mcp.servers;
9386
+ mcpRemoved = true;
9287
9387
  }
9388
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
9288
9389
  }
9289
9390
 
9290
- if (modified) {
9291
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9391
+ if (edits.length > 0) {
9392
+ writeJsoncConfig(configPath, originalText, config, edits);
9292
9393
  removedCount++;
9293
- console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
9394
+ console.log(` ${green}✓${reset} Removed GSD permissions${mcpRemoved ? ' and MCP server' : ''} from ${path.basename(configPath)}`);
9294
9395
  }
9295
9396
  } catch (e) {
9296
9397
  // Ignore JSON parse errors
@@ -9305,8 +9406,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9305
9406
  const configPath = resolveKiloConfigPath(targetDir);
9306
9407
  if (fs.existsSync(configPath)) {
9307
9408
  try {
9308
- const config = parseJsonc(fs.readFileSync(configPath, 'utf8'));
9309
- let modified = false;
9409
+ const originalText = fs.readFileSync(configPath, 'utf8');
9410
+ const config = parseJsonc(originalText);
9411
+ const edits = [];
9310
9412
 
9311
9413
  // Remove GSD permission entries
9312
9414
  if (config.permission) {
@@ -9316,7 +9418,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9316
9418
  for (const key of keys) {
9317
9419
  if (key.includes('gsd-core')) {
9318
9420
  delete config.permission[permType][key];
9319
- modified = true;
9421
+ edits.push({ path: ['permission', permType, key], remove: true });
9320
9422
  }
9321
9423
  }
9322
9424
  // Clean up empty objects
@@ -9330,8 +9432,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9330
9432
  }
9331
9433
  }
9332
9434
 
9333
- if (modified) {
9334
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9435
+ if (edits.length > 0) {
9436
+ writeJsoncConfig(configPath, originalText, config, edits);
9335
9437
  removedCount++;
9336
9438
  console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
9337
9439
  }
@@ -9373,6 +9475,67 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9373
9475
  console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
9374
9476
  }
9375
9477
 
9478
+ // #5335: the installer's own bookkeeping goes with it — the applied-migrations state and the migration
9479
+ // journal. The journal's `-backups/` folders hold copies of files the USER had modified, so a folder that
9480
+ // still has content is kept; everything GSD generated for itself (run records, rollback snapshots, empty
9481
+ // folders) is removed.
9482
+ const installStatePath = path.join(targetDir, 'gsd-install-state.json');
9483
+ if (fs.existsSync(installStatePath)) {
9484
+ fs.rmSync(installStatePath, { force: true });
9485
+ removedCount++;
9486
+ console.log(` ${green}✓${reset} Removed gsd-install-state.json`);
9487
+ }
9488
+ const journalDir = path.join(targetDir, 'gsd-migration-journal');
9489
+ if (fs.existsSync(journalDir)) {
9490
+ let journalEntriesRemoved = 0;
9491
+ try {
9492
+ for (const entry of fs.readdirSync(journalDir, { withFileTypes: true })) {
9493
+ const full = path.join(journalDir, entry.name);
9494
+ if (entry.isSymbolicLink()) continue;
9495
+ if (entry.isFile() && entry.name.endsWith('.json')) {
9496
+ fs.rmSync(full, { force: true });
9497
+ journalEntriesRemoved++;
9498
+ } else if (entry.isDirectory() && entry.name.endsWith('-rollback')) {
9499
+ fs.rmSync(full, { recursive: true, force: true });
9500
+ journalEntriesRemoved++;
9501
+ } else if (entry.isDirectory() && entry.name.endsWith('-backups')) {
9502
+ try { fs.rmdirSync(full); journalEntriesRemoved++; } catch { /* holds the user's backed-up files: keep */ }
9503
+ }
9504
+ }
9505
+ fs.rmdirSync(journalDir);
9506
+ } catch { /* a non-empty journal dir (kept backups) stays */ }
9507
+ if (journalEntriesRemoved > 0) {
9508
+ removedCount++;
9509
+ console.log(` ${green}✓${reset} Removed gsd-migration-journal/ records`);
9510
+ }
9511
+ }
9512
+
9513
+ // #5335: the installer's user-artifact staging area is empty once the install it served is over. rmdir only
9514
+ // succeeds on an empty directory, so anything still staged there is left for its owner.
9515
+ try {
9516
+ fs.rmdirSync(path.join(targetDir, '.gsd-staging', 'user-artifacts'));
9517
+ fs.rmdirSync(path.join(targetDir, '.gsd-staging'));
9518
+ } catch { /* absent or not empty */ }
9519
+
9520
+ // #5335: OpenCode never reads settings.json, but the shared-settings install step creates it as `{}`.
9521
+ // An empty one carries nothing of the user's; a populated one is left alone.
9522
+ if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
9523
+ const emptySettingsPath = path.join(targetDir, 'settings.json');
9524
+ try {
9525
+ const parsed = JSON.parse(fs.readFileSync(emptySettingsPath, 'utf8'));
9526
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {
9527
+ fs.rmSync(emptySettingsPath, { force: true });
9528
+ removedCount++;
9529
+ console.log(` ${green}✓${reset} Removed empty settings.json`);
9530
+ }
9531
+ } catch { /* absent, unreadable or populated: leave it */ }
9532
+ }
9533
+
9534
+ // #5335: the machine-wide defaults the install wrote for this runtime, when nothing else needs them.
9535
+ if (revertNonClaudeDefaults(runtime)) {
9536
+ removedCount++;
9537
+ }
9538
+
9376
9539
  if (removedCount === 0) {
9377
9540
  console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
9378
9541
  }
@@ -9384,64 +9547,30 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
9384
9547
  }
9385
9548
 
9386
9549
  /**
9387
- * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
9388
- * OpenCode supports JSONC format via jsonc-parser, so users may have comments.
9389
- * This is a lightweight inline parser to avoid adding dependencies.
9390
- */
9391
- function parseJsonc(content) {
9392
- // Strip BOM if present
9393
- if (content.charCodeAt(0) === 0xFEFF) {
9394
- content = content.slice(1);
9395
- }
9396
-
9397
- // Remove single-line and block comments while preserving strings
9398
- let result = '';
9399
- let inString = false;
9400
- let i = 0;
9401
- while (i < content.length) {
9402
- const char = content[i];
9403
- const next = content[i + 1];
9404
-
9405
- if (inString) {
9406
- result += char;
9407
- // Handle escape sequences
9408
- if (char === '\\' && i + 1 < content.length) {
9409
- result += next;
9410
- i += 2;
9411
- continue;
9412
- }
9413
- if (char === '"') {
9414
- inString = false;
9415
- }
9416
- i++;
9417
- } else {
9418
- if (char === '"') {
9419
- inString = true;
9420
- result += char;
9421
- i++;
9422
- } else if (char === '/' && next === '/') {
9423
- // Skip single-line comment until end of line
9424
- while (i < content.length && content[i] !== '\n') {
9425
- i++;
9426
- }
9427
- } else if (char === '/' && next === '*') {
9428
- // Skip block comment
9429
- i += 2;
9430
- while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
9431
- i++;
9432
- }
9433
- i += 2; // Skip closing */
9434
- } else {
9435
- result += char;
9436
- i++;
9437
- }
9550
+ * Write back an OpenCode/Kilo config the installer changed (#5334).
9551
+ *
9552
+ * A file that carries JSONC syntax (a `.jsonc` name, comments or trailing commas) is edited as text, so
9553
+ * only the keys GSD owns change and the user's annotations and layout survive. Anything the text editor
9554
+ * cannot handle, and plain JSON, keeps the previous `JSON.stringify` rewrite.
9555
+ *
9556
+ * @param {string} configPath
9557
+ * @param {string|null} originalText current file text, or null when the file is new
9558
+ * @param {object} config the mutated parsed config (used for the fallback)
9559
+ * @param {Array<{path: string[], value?: unknown, remove?: boolean}>} edits
9560
+ */
9561
+ function writeJsoncConfig(configPath, originalText, config, edits) {
9562
+ if (typeof originalText === 'string' && (configPath.endsWith('.jsonc') || hasJsoncFeatures(originalText))) {
9563
+ let text = originalText;
9564
+ for (const edit of edits) {
9565
+ text = edit.remove ? removeJsoncValue(text, edit.path) : setJsoncValue(text, edit.path, edit.value);
9566
+ if (text === null) break;
9567
+ }
9568
+ if (text !== null) {
9569
+ fs.writeFileSync(configPath, text);
9570
+ return;
9438
9571
  }
9439
9572
  }
9440
-
9441
- // Remove trailing commas before } or ]
9442
- result = result.replace(/,(\s*[}\]])/g, '$1');
9443
-
9444
- return JSON.parse(result);
9573
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9445
9574
  }
9446
9575
 
9447
9576
  /**
@@ -9463,9 +9592,11 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9463
9592
 
9464
9593
  // Read existing config or create empty object
9465
9594
  let config = {};
9595
+ let originalText = null;
9466
9596
  if (fs.existsSync(configPath)) {
9467
9597
  try {
9468
9598
  const content = fs.readFileSync(configPath, 'utf8');
9599
+ originalText = content;
9469
9600
  config = parseJsonc(content);
9470
9601
  } catch (e) {
9471
9602
  // Cannot parse - DO NOT overwrite user's config
@@ -9496,6 +9627,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9496
9627
  : `${opencodeConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
9497
9628
 
9498
9629
  let modified = false;
9630
+ const edits = [];
9499
9631
 
9500
9632
  // Configure read permission
9501
9633
  if (!config.permission.read || typeof config.permission.read !== 'object') {
@@ -9503,6 +9635,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9503
9635
  }
9504
9636
  if (config.permission.read[gsdPath] !== 'allow') {
9505
9637
  config.permission.read[gsdPath] = 'allow';
9638
+ edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
9506
9639
  modified = true;
9507
9640
  }
9508
9641
 
@@ -9512,6 +9645,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9512
9645
  }
9513
9646
  if (config.permission.external_directory[gsdPath] !== 'allow') {
9514
9647
  config.permission.external_directory[gsdPath] = 'allow';
9648
+ edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
9515
9649
  modified = true;
9516
9650
  }
9517
9651
 
@@ -9528,9 +9662,10 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9528
9662
  if (config.mcp.gsd === undefined) {
9529
9663
  config.mcp.gsd = {
9530
9664
  type: 'local',
9531
- command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
9665
+ command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server', GSD_MCP_NO_PROMPTS_FLAG],
9532
9666
  enabled: true,
9533
9667
  };
9668
+ edits.push({ path: ['mcp', 'gsd'], value: config.mcp.gsd });
9534
9669
  modified = true;
9535
9670
  }
9536
9671
 
@@ -9539,7 +9674,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
9539
9674
  }
9540
9675
 
9541
9676
  // Write config back
9542
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9677
+ writeJsoncConfig(configPath, originalText, config, edits);
9543
9678
  console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
9544
9679
  }
9545
9680
 
@@ -9562,9 +9697,11 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9562
9697
 
9563
9698
  // Read existing config or create empty object
9564
9699
  let config = {};
9700
+ let originalText = null;
9565
9701
  if (fs.existsSync(configPath)) {
9566
9702
  try {
9567
9703
  const content = fs.readFileSync(configPath, 'utf8');
9704
+ originalText = content;
9568
9705
  config = parseJsonc(content);
9569
9706
  } catch (e) {
9570
9707
  // Cannot parse - DO NOT overwrite user's config
@@ -9589,6 +9726,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9589
9726
  : `${kiloConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
9590
9727
 
9591
9728
  let modified = false;
9729
+ const edits = [];
9592
9730
 
9593
9731
  // Configure read permission
9594
9732
  if (!config.permission.read || typeof config.permission.read !== 'object') {
@@ -9596,6 +9734,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9596
9734
  }
9597
9735
  if (config.permission.read[gsdPath] !== 'allow') {
9598
9736
  config.permission.read[gsdPath] = 'allow';
9737
+ edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
9599
9738
  modified = true;
9600
9739
  }
9601
9740
 
@@ -9605,6 +9744,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9605
9744
  }
9606
9745
  if (config.permission.external_directory[gsdPath] !== 'allow') {
9607
9746
  config.permission.external_directory[gsdPath] = 'allow';
9747
+ edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
9608
9748
  modified = true;
9609
9749
  }
9610
9750
 
@@ -9613,7 +9753,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
9613
9753
  }
9614
9754
 
9615
9755
  // Write config back
9616
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
9756
+ writeJsoncConfig(configPath, originalText, config, edits);
9617
9757
  console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
9618
9758
  }
9619
9759
 
@@ -9929,7 +10069,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9929
10069
  // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
9930
10070
  // Claude local uses flatCommandsDir instead for manifest recording.
9931
10071
  const flatCommandsDir = path.join(configDir, 'commands');
9932
- const opencodeCommandDir = path.join(configDir, _hostBehaviors(runtime).flatCommandDir || 'command');
10072
+ const opencodeCommandDir = path.join(configDir, hostBehaviorsFor(runtime).flatCommandDir || 'command');
9933
10073
  // Hermes nests GSD skills under skills/gsd/ as a single category (#2841) —
9934
10074
  // already encoded in its layout descriptor's destSubpath ('skills/gsd').
9935
10075
  // All other runtimes that use the Codex-style skills layout use a flat skills/ root.
@@ -9944,7 +10084,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9944
10084
  // `options.scope` could drift; one cannot.
9945
10085
  const resolvedScope = options.scope === 'local' ? 'local' : 'global';
9946
10086
  const codexSkillsDir = _resolveSkillsRootDir(runtime, configDir, resolvedScope);
9947
- const codexSkillsManifestPrefix = _hostBehaviors(runtime).skillsManifestPrefix || 'skills/';
10087
+ const codexSkillsManifestPrefix = hostBehaviorsFor(runtime).skillsManifestPrefix || 'skills/';
9948
10088
  // #3738: resolve the ACTUAL agents-install dir honoring an agents-kind `home`
9949
10089
  // override (antigravity global → $HOME/.gemini/config/agents), mirroring
9950
10090
  // _resolveSkillsRootDir for skills. Hardcoding configDir/agents left the
@@ -9996,26 +10136,26 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
9996
10136
  // Claude local (#1367): flat gsd-*.md files at commands/ level.
9997
10137
  // Only claude local writes gsd-*.md here; global installs don't emit commands,
9998
10138
  // so this branch is a no-op for global (no matching files to find).
9999
- if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
10139
+ if (hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
10000
10140
  for (const file of fs.readdirSync(flatCommandsDir)) {
10001
10141
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
10002
10142
  manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
10003
10143
  }
10004
10144
  }
10005
10145
  }
10006
- if (_hostBehaviors(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
10146
+ if (hostBehaviorsFor(runtime).flatCommandDir && fs.existsSync(opencodeCommandDir)) {
10007
10147
  // #2329: derive the manifest key prefix from the SAME descriptor value used
10008
10148
  // to compute opencodeCommandDir above, instead of a separately-hardcoded
10009
10149
  // literal — a divergence here would silently break the manifest even after
10010
10150
  // the destSubpath descriptor is corrected (Generative Fix Divergence guard).
10011
- const flatCommandDirPrefix = _hostBehaviors(runtime).flatCommandDir || 'command';
10151
+ const flatCommandDirPrefix = hostBehaviorsFor(runtime).flatCommandDir || 'command';
10012
10152
  for (const file of fs.readdirSync(opencodeCommandDir)) {
10013
10153
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
10014
10154
  manifest.files[flatCommandDirPrefix + '/' + file] = fileHash(path.join(opencodeCommandDir, file));
10015
10155
  }
10016
10156
  }
10017
10157
  }
10018
- if (!_hostBehaviors(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
10158
+ if (!hostBehaviorsFor(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
10019
10159
  // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
10020
10160
  const skillListPrefix = 'gsd-';
10021
10161
  for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
@@ -10026,14 +10166,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10026
10166
  }
10027
10167
  }
10028
10168
  // Descriptor-driven (#2090): hash the category DESCRIPTION.md so reinstall detects drift.
10029
- if (_hostBehaviors(runtime).trackCategoryDescription) {
10169
+ if (hostBehaviorsFor(runtime).trackCategoryDescription) {
10030
10170
  const descPath = path.join(codexSkillsDir, 'DESCRIPTION.md');
10031
10171
  if (fs.existsSync(descPath)) {
10032
10172
  manifest.files['skills/gsd/DESCRIPTION.md'] = fileHash(descPath);
10033
10173
  }
10034
10174
  }
10035
10175
  }
10036
- if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
10176
+ if (hostBehaviorsFor(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
10037
10177
  const agentHashes = generateManifest(agentsDir);
10038
10178
  for (const [rel, hash] of Object.entries(agentHashes)) {
10039
10179
  const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md';
@@ -10054,7 +10194,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10054
10194
  // marker block, not the per-configDir manifest, since it lives outside it.)
10055
10195
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
10056
10196
  // hostBehaviors.clineRulesSurface.
10057
- if (_hostBehaviors(runtime).clineRulesSurface) {
10197
+ if (hostBehaviorsFor(runtime).clineRulesSurface) {
10058
10198
  for (const rel of ['.clinerules/gsd.md', '.clinerules/hooks/PreToolUse']) {
10059
10199
  const dest = path.join(configDir, rel);
10060
10200
  if (fs.existsSync(dest)) {
@@ -10075,7 +10215,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10075
10215
  // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
10076
10216
  // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
10077
10217
  // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
10078
- if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
10218
+ if (!isCodex && hostBehaviorsFor(runtime).skipSharedHooksInstall !== true) {
10079
10219
  // #3023: manifest keys must track the bundle wherever the descriptor put it,
10080
10220
  // or uninstall/saveLocalPatches silently orphan the tree.
10081
10221
  const sharedHooksDirName = resolveSharedHooksDirName(runtime);
@@ -10098,8 +10238,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10098
10238
  // to git-cmd.js (validate-commit classifier) and gsd-graphify-rebuild.sh.
10099
10239
  const hooksLibDir = path.join(hooksDir, 'lib');
10100
10240
  if (fs.existsSync(hooksLibDir)) {
10241
+ const hookLibNames = new Set(gsdHookLibFiles());
10101
10242
  for (const file of fs.readdirSync(hooksLibDir)) {
10102
- if (GSD_HOOK_LIB_FILES.includes(file)) {
10243
+ if (hookLibNames.has(file)) {
10103
10244
  manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
10104
10245
  }
10105
10246
  }
@@ -10142,13 +10283,21 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10142
10283
 
10143
10284
  // Track the OpenCode native plugin adapter (#1914) so update/drift detection
10144
10285
  // and uninstall can account for it.
10145
- const _npM = _hostBehaviors(runtime).nativePlugin;
10286
+ const _npM = hostBehaviorsFor(runtime).nativePlugin;
10146
10287
  if (_npM) {
10147
10288
  const pluginInstallPath = path.join(configDir, _npM.dir, _npM.file);
10148
10289
  if (fs.existsSync(pluginInstallPath)) {
10149
10290
  manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
10150
10291
  }
10151
10292
  }
10293
+ // The OpenCode 2.x terminal-client plugin (statusline / update banner) is tracked beside it.
10294
+ const _cliM = hostBehaviorsFor(runtime).nativeCliPlugin;
10295
+ if (_cliM) {
10296
+ const cliInstallPath = path.join(configDir, _cliM.dir, _cliM.file);
10297
+ if (fs.existsSync(cliInstallPath)) {
10298
+ manifest.files[`${_cliM.dir}/${_cliM.file}`] = fileHash(cliInstallPath);
10299
+ }
10300
+ }
10152
10301
 
10153
10302
  fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
10154
10303
  return manifest;
@@ -10373,7 +10522,7 @@ function saveLocalPatches(configDir, pristineCtx) {
10373
10522
  skillsRoot !== resolvedConfig &&
10374
10523
  !skillsRoot.startsWith(resolvedConfig + path.sep)
10375
10524
  ) {
10376
- const prefix = _hostBehaviors(patchRuntime).skillsManifestPrefix || 'skills/';
10525
+ const prefix = hostBehaviorsFor(patchRuntime).skillsManifestPrefix || 'skills/';
10377
10526
  skillsRedirect = { root: skillsRoot, prefix };
10378
10527
  }
10379
10528
  }
@@ -10596,7 +10745,7 @@ function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
10596
10745
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
10597
10746
 
10598
10747
  if (meta.files && meta.files.length > 0) {
10599
- const reapplyCommand = _hostBehaviors(runtime).reapplyCommand || '/gsd-update --reapply';
10748
+ const reapplyCommand = hostBehaviorsFor(runtime).reapplyCommand || '/gsd-update --reapply';
10600
10749
  console.log('');
10601
10750
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
10602
10751
  for (const f of meta.files) {
@@ -10624,12 +10773,12 @@ function reportInstallerMigrationResult(result) {
10624
10773
 
10625
10774
  function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10626
10775
  // #2093: isKilo dropped — Kilo's agent/model-override handling below reads
10627
- // _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag.
10776
+ // hostBehaviorsFor(runtime).frontmatterDialect === 'kilo' instead of this flag.
10628
10777
  // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
10629
10778
  // settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards
10630
10779
  // below were removed, leaving isKimi unused in this function (the kimi
10631
10780
  // local-install-deferred branch above already reads
10632
- // _hostBehaviors(runtime).localInstallDeferred instead of this flag).
10781
+ // hostBehaviorsFor(runtime).localInstallDeferred instead of this flag).
10633
10782
  // #2096: isAntigravity dropped — antigravity's agents were already
10634
10783
  // descriptor-driven (installRuntimeArtifacts), so its two legacy-agent-loop
10635
10784
  // branches (the path-rewrite skip and the converter dispatch) were
@@ -10669,7 +10818,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10669
10818
  // re-running install()) can warn again if the same condition recurs.
10670
10819
  _codexResolverModelOmittedWarned = false;
10671
10820
 
10672
- if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
10821
+ if (hostBehaviorsFor(runtime).localInstallDeferred && !isGlobal) {
10673
10822
  console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
10674
10823
  console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
10675
10824
  console.log(` Project-level Kimi install semantics remain deferred.`);
@@ -10740,7 +10889,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10740
10889
  // auto-removed on reinstall (dual-read fallback per issue #791 spec).
10741
10890
  const targetDir = isGlobal
10742
10891
  ? getGlobalConfigDir(runtime, explicitConfigDir)
10743
- : _hostBehaviors(runtime).localTargetIsProjectRoot
10892
+ : hostBehaviorsFor(runtime).localTargetIsProjectRoot
10744
10893
  ? process.cwd()
10745
10894
  : path.join(process.cwd(), dirName);
10746
10895
 
@@ -10855,7 +11004,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10855
11004
  const isWindowsHost = process.platform === 'win32';
10856
11005
  const pathPrefix = computePathPrefix({
10857
11006
  isGlobal,
10858
- isOpencode: _hostBehaviors(runtime).skipHomePrefixSubstitution === true,
11007
+ isOpencode: hostBehaviorsFor(runtime).skipHomePrefixSubstitution === true,
10859
11008
  isWindowsHost,
10860
11009
  resolvedTarget,
10861
11010
  homeDir,
@@ -10863,7 +11012,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10863
11012
  // copyWithPathReplacement, i.e. the one actually written into every
10864
11013
  // emitted command/skill/workflow body — the rewrite-engine seams below
10865
11014
  // handle re-applied surfaces, not the first install.
10866
- localDirName: _hostBehaviors(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
11015
+ localDirName: hostBehaviorsFor(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
10867
11016
  });
10868
11017
 
10869
11018
  // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
@@ -10967,7 +11116,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10967
11116
  // a core/--minimal install too (#2695), and its pass-2 sweeps remove every
10968
11117
  // gsd-* skill dir / agent file the snapshot does not claim — so an empty
10969
11118
  // minimal-mode snapshot deleted the whole surface with nothing to restore.
10970
- if (_hostBehaviors(runtime).tomlConfigInstall) {
11119
+ if (hostBehaviorsFor(runtime).tomlConfigInstall) {
10971
11120
  codexManagedSnapshotCaptured = true;
10972
11121
  const _preSkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
10973
11122
  if (fs.existsSync(_preSkillsDir)) {
@@ -11216,7 +11365,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11216
11365
  // The full restoreCodexSnapshot() (defined inside the config block) additionally
11217
11366
  // handles config.toml and the staged hooks/ tree, which are not yet touched
11218
11367
  // at this point in the pipeline.
11219
- const _codexPreConfigRollback = !_hostBehaviors(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
11368
+ const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
11220
11369
  rollbackInstallerMigrations();
11221
11370
  // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
11222
11371
  const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
@@ -11406,7 +11555,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11406
11555
  // used to describe. Claude-local remains the one special-cased path
11407
11556
  // (copyWithPathReplacement + stale-skills cleanup).
11408
11557
  const _isSkillsRuntime = (() => {
11409
- if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
11558
+ if (hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
11410
11559
  // #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
11411
11560
  // rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
11412
11561
  // — cline is the only declarant, capabilities/cline/capability.json) must not
@@ -11417,7 +11566,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11417
11566
  // spuriously fail; the `localCommandsViaRules` branch below (unchanged
11418
11567
  // messaging) and the unconditional agents-materialization block further down
11419
11568
  // (installAgentsKindStandalone) already cover this runtime/scope correctly.
11420
- if (!isGlobal && _hostBehaviors(runtime).localCommandsViaRules) return false;
11569
+ if (!isGlobal && hostBehaviorsFor(runtime).localCommandsViaRules) return false;
11421
11570
  const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
11422
11571
  const layout = cap && cap.runtime && cap.runtime.artifactLayout;
11423
11572
  if (!layout) return false;
@@ -11456,13 +11605,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11456
11605
  // marker. Write failure is non-fatal (install proceeds; warn so /gsd-surface breakage is
11457
11606
  // diagnosable) — the same contract the late write had.
11458
11607
  function _writeGsdSourceMarker(runtime, targetDir, src, isGlobal) {
11459
- if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
11608
+ if (hostBehaviorsFor(runtime).sourceMarkerFile && isGlobal) {
11460
11609
  const gsdSourceCommands = path.join(src, 'commands', 'gsd');
11461
11610
  if (fs.existsSync(gsdSourceCommands)) {
11462
11611
  try {
11463
11612
  // ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
11464
11613
  // must resolve under targetDir (parity with the other descriptor-driven writes).
11465
- const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
11614
+ const _markerPath = assertDestWithinConfigHome(targetDir, hostBehaviorsFor(runtime).sourceMarkerFile);
11466
11615
  if (hasExistingSymlinkBetween(path.resolve(targetDir), _markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
11467
11616
  throw new Error(`compatibility marker "${_markerPath}" contains an untrusted symlink`);
11468
11617
  }
@@ -11537,7 +11686,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11537
11686
  // index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
11538
11687
  // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
11539
11688
  // sufficient for Codex discovery. User-owned dirs are never touched.
11540
- if (_hostBehaviors(runtime).cleanupSkillSidecars) {
11689
+ if (hostBehaviorsFor(runtime).cleanupSkillSidecars) {
11541
11690
  cleanupCodexSkillMetadataSidecars(_skillsRootDir);
11542
11691
  }
11543
11692
 
@@ -11563,7 +11712,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11563
11712
  // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
11564
11713
  // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that
11565
11714
  // declares it, so this is byte-parity).
11566
- if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) {
11715
+ if (hostBehaviorsFor(runtime).legacyDevinSkillsCleanup && !isGlobal) {
11567
11716
  const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
11568
11717
  if (removedCount > 0) {
11569
11718
  console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
@@ -11571,12 +11720,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11571
11720
  }
11572
11721
 
11573
11722
  // Descriptor-driven (#2090): write DESCRIPTION.md for the gsd/ category after layout install
11574
- if (_hostBehaviors(runtime).writeCategoryDescription) {
11723
+ if (hostBehaviorsFor(runtime).writeCategoryDescription) {
11575
11724
  writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
11576
11725
  }
11577
11726
 
11578
11727
  // Verify installed artifacts and report
11579
- if (_hostBehaviors(runtime).reportSkillsCount) {
11728
+ if (hostBehaviorsFor(runtime).reportSkillsCount) {
11580
11729
  const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
11581
11730
  if (fs.existsSync(hermesSkillsDir)) {
11582
11731
  // Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
@@ -11590,7 +11739,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11590
11739
  } else {
11591
11740
  failures.push('skills/gsd/*');
11592
11741
  }
11593
- } else if (_hostBehaviors(runtime).verificationStyle === 'kimi') {
11742
+ } else if (hostBehaviorsFor(runtime).verificationStyle === 'kimi') {
11594
11743
  const skillsDir = path.join(targetDir, 'skills');
11595
11744
  const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
11596
11745
  if (fs.existsSync(skillsDir)) {
@@ -11614,7 +11763,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11614
11763
  // hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the
11615
11764
  // same mechanism the 'kimi' verificationStyle branch above uses; windsurf
11616
11765
  // is the only runtime that declares this value, so this is byte-parity).
11617
- } else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') {
11766
+ } else if (hostBehaviorsFor(runtime).verificationStyle === 'windsurf-workflows') {
11618
11767
  if (isGlobal) {
11619
11768
  console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
11620
11769
  } else {
@@ -11663,7 +11812,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11663
11812
  // Descriptor-driven commands/ output report (currently CodeBuddy).
11664
11813
  // Cursor retired this parallel surface in #2644 because its skills are
11665
11814
  // already slash-menu entries as well as model-invocable context.
11666
- if (_hostBehaviors(runtime).reportCommandsDir) {
11815
+ if (hostBehaviorsFor(runtime).reportCommandsDir) {
11667
11816
  const commandsDir = path.join(targetDir, 'commands');
11668
11817
  if (fs.existsSync(commandsDir)) {
11669
11818
  const cmdCount = fs.readdirSync(commandsDir)
@@ -11678,14 +11827,14 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11678
11827
  }
11679
11828
  }
11680
11829
  }
11681
- } else if (_hostBehaviors(runtime).localCommandsViaRules) {
11830
+ } else if (hostBehaviorsFor(runtime).localCommandsViaRules) {
11682
11831
  // Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
11683
11832
  // No skills/commands directory needed for local installs.
11684
11833
  // Global installs are handled above by _isSkillsRuntime (#782).
11685
11834
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
11686
11835
  // hostBehaviors.localCommandsViaRules.
11687
11836
  console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
11688
- } else if (_hostBehaviors(runtime).pluginOnlyInstall) {
11837
+ } else if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
11689
11838
  // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
11690
11839
  // registered programmatically by the native extension (pi/gsd.cjs →
11691
11840
  // extensions/gsd.js, staged separately below; the dest suffix must be
@@ -11793,8 +11942,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11793
11942
  // double-stages their plugin file. A runtime like pi, whose artifactLayout is
11794
11943
  // intentionally empty for both scopes (`_isSkillsRuntime` is false), still
11795
11944
  // needs its declared hostBehaviors.nativePlugin file copied into targetDir.
11796
- if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) {
11797
- _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
11945
+ if (!_isSkillsRuntime && hostBehaviorsFor(runtime).nativePlugin) {
11946
+ _installNativePluginIfDeclared(runtime, targetDir, hostBehaviorsFor(runtime), src);
11798
11947
  }
11799
11948
 
11800
11949
  // #2624: the .gsd-source marker is now written by _writeGsdSourceMarker()
@@ -11815,7 +11964,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11815
11964
  // hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the
11816
11965
  // only runtime that declares it, so this is byte-parity — the #1629 fix
11817
11966
  // itself is unchanged).
11818
- if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
11967
+ if (hostBehaviorsFor(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
11819
11968
  const commandsSrc = path.join(src, 'commands', 'gsd');
11820
11969
  const commandsDest = path.join(skillDest, 'commands', 'gsd');
11821
11970
  if (fs.existsSync(commandsSrc)) {
@@ -11879,7 +12028,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11879
12028
  // scopes (programmatic dispatch, no named-dispatch subagent toolkit, no
11880
12029
  // host-read markdown surface), so the resolved layout has no `agents` kind
11881
12030
  // to stage and the function returns `null` without writing anything.
11882
- if (_hostBehaviors(runtime).pluginOnlyInstall) {
12031
+ if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
11883
12032
  console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
11884
12033
  } else if (_isSkillsRuntime) {
11885
12034
  console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
@@ -11914,7 +12063,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11914
12063
  // advertising the old full agent surface even though the descriptor-driven
11915
12064
  // write above already skipped writing the .md files for a minimal-tier
11916
12065
  // resolvedProfile. Reuse the same helper that powers `--uninstall`.
11917
- if (isMinimalMode(_effectiveInstallMode) && _hostBehaviors(runtime).tomlConfigInstall) {
12066
+ if (isMinimalMode(_effectiveInstallMode) && hostBehaviorsFor(runtime).tomlConfigInstall) {
11918
12067
  const codexConfigPath = path.join(targetDir, 'config.toml');
11919
12068
  if (fs.existsSync(codexConfigPath)) {
11920
12069
  const existing = fs.readFileSync(codexConfigPath, 'utf8');
@@ -12033,7 +12182,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12033
12182
  // runtime.hostBehaviors.brandingRewrites. This site only
12034
12183
  // rewrites the two brand-name keys (no `.claude/` here — the
12035
12184
  // config-dir replace above already handled path fragments).
12036
- const _b2 = _hostBehaviors(runtime).brandingRewrites;
12185
+ const _b2 = hostBehaviorsFor(runtime).brandingRewrites;
12037
12186
  if (_b2) {
12038
12187
  content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']);
12039
12188
  content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']);
@@ -12192,7 +12341,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12192
12341
  // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
12193
12342
  // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
12194
12343
  // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
12195
- if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
12344
+ if (!isCodex && hostBehaviorsFor(runtime).skipSharedHooksInstall !== true) {
12196
12345
  if (!installSharedHooksBundle(targetDir)) {
12197
12346
  failures.push('hooks');
12198
12347
  }
@@ -12359,7 +12508,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12359
12508
  }
12360
12509
 
12361
12510
  // Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
12362
- if (!_hostBehaviors(runtime).ownsClaudePaths) {
12511
+ if (!hostBehaviorsFor(runtime).ownsClaudePaths) {
12363
12512
  const leakedPaths = [];
12364
12513
  // Only scan files that were written by this install (manifest-tracked).
12365
12514
  // Scanning the entire targetDir can match user-authored content that
@@ -12392,7 +12541,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12392
12541
  // verification backstop. The `_GSD_RUNTIME_ROOT`/`$PREFERRED_CONFIG_DIR`
12393
12542
  // fallback chains and prose `.claude` mentions carry no `@~/` prefix and
12394
12543
  // are deliberately untouched, as is CHANGELOG.md.
12395
- if (runtime === 'codex') {
12544
+ if (hostBehaviorsFor(runtime).rewriteClaudeAtIncludes) {
12396
12545
  for (const relPath of manifestFiles) {
12397
12546
  const fileName = path.basename(relPath);
12398
12547
  if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
@@ -13041,7 +13190,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13041
13190
  // stop, subagentStart, subagentStop) via runtime-hooks-surface.cts, which reads
13042
13191
  // the event list from the descriptor-driven adapter module.
13043
13192
  const cursorHookResult = writeCursorHooksJson(targetDir, src, {
13044
- managedHookEvents: _hostBehaviors(runtime).managedHookEvents,
13193
+ managedHookEvents: hostBehaviorsFor(runtime).managedHookEvents,
13045
13194
  });
13046
13195
  if (cursorHookResult.changed) {
13047
13196
  console.log(` ${green}✓${reset} Configured Cursor lifecycle hooks (sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop)`);
@@ -13110,7 +13259,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13110
13259
  }
13111
13260
  const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
13112
13261
  const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
13113
- const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
13262
+ const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts, contextWarnings: runtimeSupportsContextWarnings(runtime) });
13114
13263
  configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
13115
13264
  if (kimiHooksResult.changed) {
13116
13265
  console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
@@ -13134,9 +13283,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13134
13283
  // reports success and the user is left with the very breakage the opt-in
13135
13284
  // exists to prevent. Verified reproducible before this guard existed.
13136
13285
  const kimiInstalledThisRun = selectedRuntimes.includes('kimi');
13137
- if (hasReclaimKimiLegacy && runtime === 'kimi-code' && kimiInstalledThisRun) {
13286
+ if (hasReclaimKimiLegacy && hostBehaviorsFor(runtime).reclaimsKimiLegacyHooksRoot && kimiInstalledThisRun) {
13138
13287
  console.log(` ${dim}•${reset} Skipped --reclaim-kimi-legacy: this run also installs --kimi, so ${resolveKimiHooksTomlDir({ runtime: 'kimi' })} is a live Kimi CLI install`);
13139
- } else if (hasReclaimKimiLegacy && runtime === 'kimi-code') {
13288
+ } else if (hasReclaimKimiLegacy && hostBehaviorsFor(runtime).reclaimsKimiLegacyHooksRoot) {
13140
13289
  const legacyKimiRoot = resolveKimiHooksTomlDir({ runtime: 'kimi' });
13141
13290
  // Both roots honor their own env override (KIMI_SHARE_DIR /
13142
13291
  // KIMI_CODE_HOME). A user who points both at ONE directory collapses
@@ -13212,15 +13361,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13212
13361
  // #2870: the CURRENT scope's settings filename is sourced from the Install
13213
13362
  // Scope Module (_installScope.settingsFile, resolveScope's per-scope field)
13214
13363
  // instead of indexing _scopedSettings by hand. _scopedSettings itself is
13215
- // retained unchanged as the #338-privacy fail-safe path: _hostBehaviors
13216
- // already degrades to FALLBACK_HOST_BEHAVIORS (see that constant's comment
13217
- // above) when the registry fails to load, whereas resolveScope's registry
13364
+ // retained unchanged as the #338-privacy fail-safe path: hostBehaviorsFor
13365
+ // already degrades to its #338 floor (see runtime-name-policy.cts)
13366
+ // when the registry fails to load, whereas resolveScope's registry
13218
13367
  // lookup throws in that same scenario (_installScope is null when it did).
13219
13368
  // Falling back to _scopedSettings[_installScopeId] there — and keeping the
13220
13369
  // non-local-claude branch's expression untouched — means this is
13221
13370
  // byte-identical to the pre-migration computation in every case, including
13222
13371
  // the broken-registry fail-safe floor.
13223
- const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
13372
+ const _scopedSettings = hostBehaviorsFor(runtime).settingsFileByScope || null;
13224
13373
  const _currentScopeSettingsFile = _installScope
13225
13374
  ? _installScope.settingsFile
13226
13375
  : (_scopedSettings ? (_scopedSettings[_installScopeId] ?? null) : null);
@@ -13346,7 +13495,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13346
13495
  // Descriptor-driven (ADR-1239 / #2096): hookPathStyle comes from the
13347
13496
  // runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
13348
13497
  // check inside projectLocalHookPrefix.
13349
- const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
13498
+ const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: hostBehaviorsFor(runtime).hookPathStyle });
13350
13499
  const settingsEntrypoints = [];
13351
13500
  const hookOpts = {
13352
13501
  portableHooks: hasPortableHooks,
@@ -13392,6 +13541,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13392
13541
  const updateCheckCommand = isGlobal
13393
13542
  ? buildHookCommand(targetDir, 'gsd-check-update.js', hookOpts)
13394
13543
  : localCmd('gsd-check-update.js');
13544
+ // #4840: no metrics writer on this host -> no monitor to register (see runtimeSupportsContextWarnings).
13545
+ const contextWarnings = runtimeSupportsContextWarnings(runtime);
13546
+ // The command stays built: the extended lifecycle events (SubagentStop, PreCompact, ...) still route to it.
13395
13547
  const contextMonitorCommand = isGlobal
13396
13548
  ? buildHookCommand(targetDir, 'gsd-context-monitor.js', hookOpts)
13397
13549
  : localCmd('gsd-context-monitor.js');
@@ -13436,6 +13588,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13436
13588
  hooksSurface: plan.hooksSurface,
13437
13589
  updateCheckCommand,
13438
13590
  contextMonitorCommand,
13591
+ contextWarnings,
13439
13592
  promptGuardCommand,
13440
13593
  readGuardCommand,
13441
13594
  readInjectionScannerCommand,
@@ -13444,12 +13597,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13444
13597
  localCmd,
13445
13598
  localShellCmd,
13446
13599
  });
13600
+ if (!contextWarnings) {
13601
+ console.log(` ${dim}↳${reset} ${getRuntimeLabel(runtime)}: agent-facing context warnings are unsupported (no statusline writes the context metrics the monitor reads), so the context monitor hook is not registered`);
13602
+ }
13447
13603
 
13448
13604
  // Compute the update-banner hook command alongside the others so
13449
13605
  // installAllRuntimes can register it at finalize time when the user opts
13450
13606
  // in (#2795). Computed here (not in finishInstall) so the same buildHookCommand
13451
13607
  // / localCmd resolution logic is shared with the other JS hooks.
13452
- const updateBannerCommand = _hostBehaviors(runtime).skipUpdateBannerCommand
13608
+ const updateBannerCommand = hostBehaviorsFor(runtime).skipUpdateBannerCommand
13453
13609
  ? null
13454
13610
  : (isGlobal
13455
13611
  ? buildHookCommand(targetDir, 'gsd-update-banner.js', hookOpts)
@@ -13612,7 +13768,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13612
13768
  // on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
13613
13769
  // #2094: isTrae dropped — unused in this function.
13614
13770
  // #2095: isKimi dropped — the Kimi "Done!" banner below reads
13615
- // _hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
13771
+ // hostBehaviorsFor(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
13616
13772
  // #2096: isAntigravity dropped — unused in this function.
13617
13773
  // #2098: isCodebuddy dropped — unused in this function.
13618
13774
  // #2099: isCopilot dropped — unused in this function.
@@ -13633,7 +13789,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13633
13789
  // runtime — kept for a caller that invokes finishInstall directly.
13634
13790
  assertConfiguredEntrypoints(bannerOpts.configuredEntrypoints);
13635
13791
 
13636
- if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
13792
+ if (shouldInstallStatusline && plan.writesSharedSettings && !hostBehaviorsFor(runtime).skipSettingsUi) {
13637
13793
  if (!isGlobal && !forceStatusline) {
13638
13794
  // Local installs skip statusLine by default: repo settings.json takes precedence over
13639
13795
  // profile-level settings.json in Claude Code, so writing here would silently clobber
@@ -13659,7 +13815,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13659
13815
  // settings.json hooks block — opencode/kilo/codex/cursor/windsurf/trae/
13660
13816
  // cline either lack the surface or use a different config schema.
13661
13817
  const { shouldInstallBanner, bannerCommand } = bannerOpts;
13662
- if (shouldInstallBanner && settings && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
13818
+ if (shouldInstallBanner && settings && plan.writesSharedSettings && !hostBehaviorsFor(runtime).skipSettingsUi) {
13663
13819
  if (!bannerCommand) {
13664
13820
  console.warn(` ${yellow}⚠${reset} Skipped update banner registration — Node executable path unavailable. See #2979 / #3002.`);
13665
13821
  } else {
@@ -13688,13 +13844,13 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13688
13844
  // Merges GSD-owned entries non-destructively (preserves existing user permissions).
13689
13845
  // Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
13690
13846
  // settings.json but use different runtimes and do not use these permission strings.
13691
- if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
13847
+ if (hostBehaviorsFor(runtime).permissionsSchema === 'claude') {
13692
13848
  mergeClaudePermissions(settings);
13693
13849
  }
13694
13850
 
13695
13851
  // #2097 UPGRADE 3 (transport:mcp): companion MCP server for runtimes that host
13696
13852
  // MCP in settings.json (Augment). settings.json is golden-excluded, so no golden change.
13697
- if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
13853
+ if (hostBehaviorsFor(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
13698
13854
  mergeGsdMcpServerIntoSettings(settings);
13699
13855
  }
13700
13856
 
@@ -13712,6 +13868,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13712
13868
  // Configure OpenCode permissions
13713
13869
  if (plan.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) {
13714
13870
  configureOpencodePermissions(isGlobal, configDir);
13871
+ // ADR-5331: say which OpenCode generation this install will run under. Informational only —
13872
+ // a failed probe never affects the install.
13873
+ try {
13874
+ const ocv = require('../gsd-core/bin/lib/opencode-version.cjs');
13875
+ const gen = ocv.detectOpencodeGeneration();
13876
+ if (gen.generation !== null && typeof configDir === 'string' && configDir) {
13877
+ recordHostGeneration(configDir, { generation: gen.generation, version: gen.version });
13878
+ }
13879
+ if (gen.generation === 2) {
13880
+ console.log(` ${green}✓${reset} OpenCode ${gen.version || '2.x'} detected — GSD loads its OpenCode 2.x plugin entrypoint`);
13881
+ } else if (gen.generation === 1) {
13882
+ console.log(` ${green}✓${reset} OpenCode ${gen.version || '1.x'} detected — GSD loads its OpenCode 1.x plugin entrypoint`);
13883
+ }
13884
+ // ADR-5331 D6 / E8: OpenCode 2.x migrates its data directory one way. Say so before it happens, and say
13885
+ // when 1.x is installed over a data directory 2.x already migrated (1.x cannot open it).
13886
+ const dataDir = ocv.resolveOpencodeDataDir();
13887
+ const notice = ocv.describeDataDirNotice(gen.generation, ocv.classifyOpencodeDatabase(path.join(dataDir, 'opencode.db')), dataDir);
13888
+ if (notice) {
13889
+ console.log(notice.level === 'warn' ? ` ${yellow}⚠${reset} ${notice.text}` : ` ${dim}${notice.text}${reset}`);
13890
+ }
13891
+ // Phase 3 (#5339): OpenCode 1.x is bug-fix-only until the freeze date. The `opencode` runtime only —
13892
+ // this block does not run for Kilo.
13893
+ const deprecation = ocv.describeDeprecationNotice(gen.generation);
13894
+ if (deprecation) console.log(` ${yellow}⚠${reset} ${deprecation}`);
13895
+ } catch { /* detection is advisory */ }
13715
13896
  }
13716
13897
 
13717
13898
  // Configure Kilo permissions
@@ -13734,18 +13915,29 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13734
13915
  // generation reads it). This call is idempotent (preserves existing values).
13735
13916
  writeNonClaudeDefaults(runtime);
13736
13917
 
13737
- // program + command are now single-source lookups (ADR-1239 Phase B / #1679):
13738
- // program is the runtime display label; command is the per-host /gsd-new-project
13739
- // invocation syntax.
13918
+ // program is the runtime display label (ADR-1239 Phase B / #1679). The command
13919
+ // is generated from the surface this runtime registered in this install scope
13920
+ // (#5215, ADR-5057 §5 Phase 12) — a runtime that registers no new-project
13921
+ // trigger is told so instead of being sent to a command that does not exist (#4567).
13740
13922
  const program = getRuntimeLabel(runtime);
13741
- const command = getRuntimeNewProjectCommand(runtime);
13923
+ const advertised = resolveAdvertisedNewProject(runtime, isGlobal ? 'global' : 'local');
13924
+ const command = advertised.kind === 'command' ? advertised.command : null;
13925
+ // Host-specific launch/restart steps below stay; only the new-project clause
13926
+ // changes when the runtime registered no such command.
13927
+ const noNewProject = advertised.kind === 'unregistered'
13928
+ ? `${program} registers no new-project command in this scope${advertised.nativeCommand ? ` (its native extension registers ${cyan}${advertised.nativeCommand}${reset})` : ''}.`
13929
+ : '';
13742
13930
 
13743
13931
  // Claude Code global installs use the skills/ format (CC 2.1.88+).
13744
13932
  // Restart is required for CC to pick up newly-installed skills, and the
13745
13933
  // slash-menu surface depends on CC version — so the instruction needs to
13746
13934
  // cover both invocation paths to avoid #2957-style "no commands appear".
13747
- if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
13748
- console.log(`
13935
+ if (hostBehaviorsFor(runtime).skillsGlobalOnboarding && isGlobal) {
13936
+ console.log(command === null ? `
13937
+ ${green}Done!${reset} Restart ${program}. ${noNewProject}
13938
+
13939
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13940
+ ` : `
13749
13941
  ${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.
13750
13942
 
13751
13943
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -13753,9 +13945,13 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13753
13945
  return;
13754
13946
  }
13755
13947
 
13756
- if (_hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file') {
13948
+ if (hostBehaviorsFor(runtime).doneBannerStyle === 'kimi-agent-file') {
13757
13949
  const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml';
13758
- console.log(`
13950
+ console.log(command === null ? `
13951
+ ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}. ${noNewProject}
13952
+
13953
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13954
+ ` : `
13759
13955
  ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}.
13760
13956
 
13761
13957
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -13763,7 +13959,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
13763
13959
  return;
13764
13960
  }
13765
13961
 
13766
- console.log(`
13962
+ console.log(command === null ? `
13963
+ ${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})` : ''}.
13964
+
13965
+ ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
13966
+ ` : `
13767
13967
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
13768
13968
 
13769
13969
  ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r
@@ -14713,9 +14913,6 @@ module.exports = {
14713
14913
  install,
14714
14914
  installAllRuntimes,
14715
14915
  uninstall,
14716
- // #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
14717
- _resolveHostBehaviors,
14718
- FALLBACK_HOST_BEHAVIORS,
14719
14916
  // #3023 — shared hook bundle directory name, descriptor-driven
14720
14917
  SHARED_HOOKS_DIR_DEFAULT,
14721
14918
  resolveSharedHooksDirName,