@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -21,21 +21,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  const node_path_1 = __importDefault(require("node:path"));
22
22
  const node_os_1 = __importDefault(require("node:os"));
23
23
  const node_fs_1 = __importDefault(require("node:fs"));
24
+ // #2874 (ADR-58 cleanup phase): route this module's content-rewrite-pass fs
25
+ // calls through the installRuntimeArtifacts call tree's injectable seam —
26
+ // see install-fs-adapter.cts's module doc. Resolves to real `node:fs` unless
27
+ // the top-level installRuntimeArtifacts call injected a `deps.fs`. These
28
+ // walkers operate on already-staged temp directories (never the real GSD
29
+ // source tree or the real install destination directly), so routing them is
30
+ // unconditionally safe.
31
+ const installFsAdapter = require("./install-fs-adapter.cjs");
32
+ const { installFs, mkInstallTempDir } = installFsAdapter;
24
33
  const commandRoster = require("./command-roster.cjs");
25
34
  const { readGsdCommandNames, transformContentToHyphen } = commandRoster;
26
35
  const runtimeNamePolicy = require("./runtime-name-policy.cjs");
27
36
  const { getDirName } = runtimeNamePolicy;
28
37
  const capabilityRegistry = require("./capability-registry.cjs");
38
+ const hostIntegration = require("./host-integration.cjs");
29
39
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
40
+ const pattern_cjs_1 = require("./pattern.cjs");
41
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
42
+ // #2870: install-scope.cts is a leaf-tier sibling (imports only
43
+ // runtime-homes.cjs + node builtins, never this module) — no cycle. See the
44
+ // isGlobal sites below for why the boolean projection is centralized here too.
45
+ const install_scope_cjs_1 = require("./install-scope.cjs");
46
+ // #2875 Part 2: install-effort-resolver.cjs is a leaf-tier sibling (#2071) —
47
+ // used by applyAgentFrontmatterExtensions below to read the SAME merged
48
+ // effort config the install-time Claude .md injection has always read,
49
+ // without this module reaching upward into bin/install.js (ADR-1508).
50
+ const installEffortResolver = require("./install-effort-resolver.cjs");
51
+ const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort, _getGsdEffortCatalog } = installEffortResolver;
30
52
  // #1383: resolve GSD's version WITHOUT a top-level
31
53
  // `require('../../../package.json')`. That require ran at module load on every
32
54
  // gsd-tools invocation (this module sits in the gsd-tools loader chain) and
33
55
  // threw `Cannot find module '../../../package.json'` on runtimes whose root has
34
- // no package.json — notably Codex, where the installer omits the synthetic root
35
- // package.json — taking the entire CLI down before it did anything. And even
36
- // where it resolved (Claude's synthetic `{"type":"commonjs"}`), there is no
37
- // `version` field, so the single consumer below already emitted
38
- // `version: undefined`. Resolve lazily and defensively instead:
56
+ // no package.json — originally just Codex, where the installer never wrote the
57
+ // synthetic root package.json; since #2544 that is true of EVERY runtime, as
58
+ // GSD's markers moved into `hooks/` and the native plugin dir and the config
59
+ // root is no longer written at all — taking the entire CLI down before it did
60
+ // anything. And even where it used to resolve (the synthetic
61
+ // `{"type":"commonjs"}`), there is no `version` field, so the single consumer
62
+ // below already emitted `version: undefined`. Resolve lazily and defensively
63
+ // instead:
39
64
  // 1. Installed trees carry <root>/gsd-core/VERSION (written by the installer);
40
65
  // this module lives at <root>/gsd-core/bin/lib, so VERSION is two dirs up.
41
66
  // 2. The source / npm-package tree has no gsd-core/VERSION but carries a real
@@ -267,11 +292,8 @@ function buildKiloAgentPermissionBlock(claudeTools) {
267
292
  }
268
293
  return lines;
269
294
  }
270
- function escapeRegExp(value) {
271
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
272
- }
273
295
  function replaceRelativePathReference(content, fromPath, toPath) {
274
- const escapedPath = escapeRegExp(fromPath);
296
+ const escapedPath = (0, pattern_cjs_1.escapeRegex)(fromPath);
275
297
  return content.replace(new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'), (_, prefix) => `${prefix}${toPath}`);
276
298
  }
277
299
  /**
@@ -364,9 +386,6 @@ function skillFrontmatterName(skillDirName) {
364
386
  // Return the hyphen form as-is (gsd-<cmd>) — canonical since #2808.
365
387
  return skillDirName;
366
388
  }
367
- function normalizeClaudeSkillEffort(effort) {
368
- return effort === 'xhigh' ? 'max' : effort;
369
- }
370
389
  /**
371
390
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
372
391
  * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -421,10 +440,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
421
440
  const description = extractFrontmatterField(frontmatter, 'description') || '';
422
441
  const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
423
442
  const agent = extractFrontmatterField(frontmatter, 'agent');
424
- // #769: preserve context: and effort: from source command files so they
425
- // are emitted into the installed SKILL.md frontmatter unchanged.
443
+ // #769: preserve context: from source command files so it is emitted into
444
+ // the installed SKILL.md frontmatter unchanged. (#3151: effort: is no longer
445
+ // emitted into skill frontmatter — a static effort value changes
446
+ // output_config.effort on invocation and invalidates the caller's prompt
447
+ // cache at both scope boundaries; the reporter's owned measurement confirms
448
+ // the mechanism. The separate agent-effort surface is tracked by #3160.)
426
449
  const context = extractFrontmatterField(frontmatter, 'context');
427
- const effort = extractFrontmatterField(frontmatter, 'effort');
428
450
  // Preserve allowed-tools as YAML multiline list (Claude native format)
429
451
  const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
430
452
  let toolsBlock = '';
@@ -464,19 +486,100 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
464
486
  fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
465
487
  if (agent)
466
488
  fm += `agent: ${agent}\n`;
467
- // #769: emit context: and effort: when present so the runtime can honour
468
- // them natively (context: fork = isolated subagent window; effort: =
469
- // token-budget tier). Fields are Claude-specific; unknown frontmatter
470
- // fields are silently ignored by other runtimes (backward-compatible).
489
+ // #769: emit context: when present so the runtime can honour it natively
490
+ // (context: fork = isolated subagent window). Claude-specific; unknown
491
+ // frontmatter fields are silently ignored by other runtimes (backward-compatible).
492
+ // (#3151: effort: is intentionally NOT emitted into skill frontmatter — a
493
+ // static effort value changes output_config.effort on invocation and
494
+ // invalidates the caller's prompt cache at both scope boundaries.)
471
495
  if (context)
472
496
  fm += `context: ${context}\n`;
473
- if (effort)
474
- fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
475
497
  if (toolsBlock)
476
498
  fm += toolsBlock;
477
499
  fm += '---';
478
500
  return `${fm}\n${normalizedBody}`;
479
501
  }
502
+ // #2873 (4b) — spec-root reachability. Matches ONLY a line that is a real
503
+ // `@~/.claude/gsd-core/workflows/<stem>.md` include: line-start `@`, exact
504
+ // spec-root shape, nothing else on the line. This is deliberately narrower
505
+ // than "any line mentioning gsd-core/workflows" so prose mentions and
506
+ // `references/`/`templates/`/`@.planning/...` includes are never touched
507
+ // (rows 24/25). CRLF-safe: an optional trailing `\r` is captured and
508
+ // preserved rather than dropped.
509
+ const WORKFLOW_SPEC_ROOT_INCLUDE_RE = /^@~\/\.claude\/gsd-core\/workflows\/([A-Za-z0-9._-]+)\.md[ \t]*(\r?)$/gm;
510
+ /**
511
+ * Rewrite a static global-scope Claude skill `@`-include of the command's own
512
+ * workflow spec into an imperative two-step resolution the agent performs at
513
+ * runtime: prefer the project-local spec (cwd-relative), fall back to the
514
+ * global spec, and treat "neither exists" as a visible failure rather than a
515
+ * silent no-spec proceed.
516
+ *
517
+ * WHY this can't stay a static `@`-include (even a relative one): Claude Code
518
+ * documents relative `@`-paths as resolving against the file *containing* the
519
+ * import, which for a global skill is `~/.claude/skills/gsd-<stem>/` — not
520
+ * the project's working directory. `@./.claude/...` would therefore always
521
+ * resolve inside the skill's own install directory, never the project, so
522
+ * there is no static include syntax that can express "prefer local, fall
523
+ * back to global". This function exists precisely so that resolution can be
524
+ * performed by the agent, not the host's pre-expansion.
525
+ *
526
+ * Scope-free by design: this function does not know or care whether it is
527
+ * being applied to a global or local artifact, or which runtime — that
528
+ * judgment belongs to the caller (`skillsKind` in
529
+ * `runtime-artifact-layout.cts`, the one site that knows install scope).
530
+ * Applying it to a body with no workflow include is a no-op (row 26); a body
531
+ * with two independent workflow includes has each rewritten independently
532
+ * (row 27); an include inside a fenced code block or wrapped in inline
533
+ * backticks is left untouched (the backtick case is already excluded by the
534
+ * line-start anchor, since a backtick-wrapped line does not begin with `@`).
535
+ * Idempotent: the replacement text never begins with `@` and never matches
536
+ * `WORKFLOW_SPEC_ROOT_INCLUDE_RE`, so re-applying this function to its own
537
+ * output is a no-op.
538
+ *
539
+ * Fence detection reuses `scanFencedBlocks` (markdown-sectionizer.cts) — the
540
+ * same CommonMark-correct state machine `stripFencedCode`/`extractFencedBlock`
541
+ * are built on — instead of a hand-rolled "any delimiter line toggles
542
+ * open/closed" tracker. A naive toggle is wrong under CommonMark: a fence
543
+ * opened with ``` is NOT closed by a ~~~ line (closer must share the
544
+ * opener's delimiter character and have run length >= the opener's), so a
545
+ * mismatched delimiter is fence CONTENT, not a boundary. #2873 review.
546
+ */
547
+ function resolveSpecRootReference(body) {
548
+ if (typeof body !== 'string' || body.length === 0)
549
+ return body;
550
+ if (!body.includes('@~/.claude/gsd-core/workflows/'))
551
+ return body;
552
+ // Collect [start, end) character-offset ranges covered by fenced code
553
+ // blocks so matches inside them are skipped. An unterminated trailing
554
+ // fence covers to the end of the string (still "inside a fence").
555
+ const lines = body.split('\n');
556
+ const lineStartOffsets = [];
557
+ {
558
+ let offset = 0;
559
+ for (const line of lines) {
560
+ lineStartOffsets.push(offset);
561
+ offset += line.length + 1; // +1 for the '\n' separator
562
+ }
563
+ }
564
+ const fenceRanges = (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines).map(({ openLineIdx, closeLineIdx }) => {
565
+ const start = lineStartOffsets[openLineIdx];
566
+ const end = closeLineIdx === -1
567
+ ? body.length
568
+ : lineStartOffsets[closeLineIdx] + lines[closeLineIdx].length;
569
+ return [start, end];
570
+ });
571
+ const isInsideFence = (offset) => fenceRanges.some(([start, end]) => offset >= start && offset < end);
572
+ return body.replace(WORKFLOW_SPEC_ROOT_INCLUDE_RE, (match, stem, cr, offset) => {
573
+ if (isInsideFence(offset))
574
+ return match;
575
+ return (`To load this command's workflow spec: check for ` +
576
+ `\`.claude/gsd-core/workflows/${stem}.md\` relative to the current working ` +
577
+ `directory first (project-local); if it is not there, fall back to ` +
578
+ `\`~/.claude/gsd-core/workflows/${stem}.md\` (the global install). If ` +
579
+ `neither file exists, stop — a workflow spec is required and none was found.` +
580
+ cr);
581
+ });
582
+ }
480
583
  function normalizeKimiSkillName(skillName) {
481
584
  let text = String(skillName || '').trim().toLowerCase();
482
585
  if (text.startsWith('/'))
@@ -492,7 +595,7 @@ function normalizeKimiSkillName(skillName) {
492
595
  function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
493
596
  if (!Array.isArray(cmdNames) || cmdNames.length === 0)
494
597
  return content;
495
- const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp);
598
+ const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(pattern_cjs_1.escapeRegex);
496
599
  const commandGroup = commands.join('|');
497
600
  const colonPattern = new RegExp(`(?<![A-Za-z0-9_/:.-])/?gsd:(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
498
601
  const hyphenPattern = new RegExp(`(?:/|\\$)gsd-(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
@@ -848,8 +951,9 @@ function convertClaudeToCursorMarkdown(content) {
848
951
  // Remove Claude Code-specific bug workarounds before brand replacement
849
952
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
850
953
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
851
- // Replace "Claude Code" brand references with "Cursor"
852
- converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
954
+ // Replace "Claude Code" brand references with "Cursor" — #2284(b): skips
955
+ // <runtime_compatibility> comparison-table content (protected region).
956
+ converted = applyClaudeCodeBrandSwap(converted, 'Cursor');
853
957
  return converted;
854
958
  }
855
959
  function getCursorSkillAdapterHeader(skillName) {
@@ -890,39 +994,41 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
890
994
  description = toSingleLine(description);
891
995
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
892
996
  const adapter = getCursorSkillAdapterHeader(skillName);
893
- // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
894
- // menu (it defaults to true). Cursor also writes a commands/ surface (#785),
895
- // and surfacing both duplicated every /gsd-* entry. This mirrors the #789
896
- // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
897
- // stay model-invocable background knowledge. (user-invocable:false hides from
898
- // '/' while keeping model invocation — distinct from disable-model-invocation.)
899
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
900
- }
901
- /**
902
- * Convert a Claude Code command to a Cursor 1.6 slash command (#785).
903
- *
904
- * Cursor slash commands live in `.cursor/commands/<name>.md` and are
905
- * plain markdown — no YAML frontmatter, no adapter header. The filename
906
- * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
907
- *
908
- * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
909
- * converter (tool renames, brand substitution, slash-command normalisation),
910
- * then strips the YAML frontmatter block so only the prose body remains.
911
- *
912
- * @param {string} content raw Claude Code command markdown (may have frontmatter)
913
- * @param {string} _commandName the target command name (unused; present for
914
- * API symmetry with other converters so the runtime-artifact-layout stage
915
- * function can call it uniformly)
916
- * @returns {string} plain markdown body, no frontmatter
917
- */
918
- function convertClaudeCommandToCursorCommand(content, _commandName) {
919
- const converted = convertClaudeToCursorMarkdown(content);
920
- const { body } = extractFrontmatterAndBody(converted);
921
- return body.trimStart();
997
+ // Cursor skills are both slash-invocable and model-invocable. Do not emit the
998
+ // unsupported `user-invocable` field: it is ignored by Cursor and previously
999
+ // hid the real cause of duplicate entries, the parallel commands/ surface
1000
+ // retired in #2644.
1001
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
922
1002
  }
923
1003
  // --- Windsurf converters ---
924
1004
  // Windsurf uses a tool set similar to Cursor.
925
1005
  // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
1006
+ // #2931: ported from bin/install.js's local Windsurf converter copy, which had
1007
+ // picked up the #2284(b) protected-region fix that this exported source never
1008
+ // received. Binding bin/install.js's Windsurf family to these exports (see
1009
+ // tests/install-runtime-artifacts.test.cjs reference-identity assertions)
1010
+ // without this would have silently regressed live installs: `Claude Code`
1011
+ // mentions inside a `<runtime_compatibility>` comparison table would start
1012
+ // getting brand-swapped again. Split `content` on the protected-block regex,
1013
+ // brand-swap only the gap text between (and around) matches, then rejoin
1014
+ // gap+block alternately — no placeholder/sentinel token involved.
1015
+ const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
1016
+ function applyClaudeCodeBrandSwap(content, brandName) {
1017
+ if (!brandName)
1018
+ return content;
1019
+ let result = '';
1020
+ let lastIndex = 0;
1021
+ RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
1022
+ let m;
1023
+ while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
1024
+ const gap = content.slice(lastIndex, m.index);
1025
+ result += gap.replace(/\bClaude Code\b/g, brandName);
1026
+ result += m[0]; // protected block, verbatim — never brand-swapped
1027
+ lastIndex = m.index + m[0].length;
1028
+ }
1029
+ result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
1030
+ return result;
1031
+ }
926
1032
  function convertSlashCommandsToWindsurfSkillMentions(content) {
927
1033
  // Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
928
1034
  return content.replace(/gsd:/gi, 'gsd-');
@@ -953,8 +1059,9 @@ function convertClaudeToWindsurfMarkdown(content) {
953
1059
  // Remove Claude Code-specific bug workarounds before brand replacement
954
1060
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
955
1061
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
956
- // Replace "Claude Code" brand references with "Windsurf"
957
- converted = converted.replace(/\bClaude Code\b/g, 'Windsurf');
1062
+ // Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
1063
+ // <runtime_compatibility> comparison-table content (protected region).
1064
+ converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
958
1065
  return converted;
959
1066
  }
960
1067
  function getWindsurfSkillAdapterHeader(skillName) {
@@ -993,10 +1100,46 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
993
1100
  }
994
1101
  }
995
1102
  description = toSingleLine(description);
996
- const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1103
+ // #2931: code-point-safe truncation (see truncateWindsurfWorkflowDescription
1104
+ // below) — a raw UTF-16 `slice(0, 177)` can bisect a surrogate pair and
1105
+ // emit a lone surrogate on re-encode. Same exact bounds as before (>180
1106
+ // chars -> first 177 code points + '...'), just harmonized with the
1107
+ // sibling Windsurf workflow converter's helper instead of duplicating the
1108
+ // surrogate-splitting idiom here.
1109
+ const shortDescription = truncateWindsurfWorkflowDescription(description);
997
1110
  const adapter = getWindsurfSkillAdapterHeader(skillName);
998
1111
  return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
999
1112
  }
1113
+ // #2931: cap on the Windsurf workflow's only unbounded input (the frontmatter
1114
+ // `description`). Shared (not just mirrored) by convertClaudeCommandToWindsurfSkill
1115
+ // above — both converters call truncateWindsurfWorkflowDescription below so
1116
+ // there is exactly one code-point-safe truncation idiom, not two.
1117
+ const WINDSURF_WORKFLOW_DESCRIPTION_MAX = 180;
1118
+ function truncateWindsurfWorkflowDescription(description) {
1119
+ // Multi-byte safe: slice by Unicode code points (`Array.from`), never by
1120
+ // raw UTF-16 index — an index-based slice can bisect a surrogate pair and
1121
+ // emit a lone surrogate / U+FFFD on re-encode. See #2931.
1122
+ const codePoints = Array.from(description);
1123
+ if (codePoints.length <= WINDSURF_WORKFLOW_DESCRIPTION_MAX)
1124
+ return description;
1125
+ return `${codePoints.slice(0, WINDSURF_WORKFLOW_DESCRIPTION_MAX - 3).join('')}...`;
1126
+ }
1127
+ // #2931: SEPARATE size control from the #1615 security regex below — do not
1128
+ // fold the two together or make either conditional on the other. The #1615
1129
+ // regex constrains commandName's CHARACTER CLASS but not its LENGTH, and
1130
+ // commandName is interpolated into the emitted template three times (the
1131
+ // `# <commandName>` heading, the `@.../<stem>.md` @-reference target, and the
1132
+ // trailing "after /<commandName>" mention) — so an unbounded commandName
1133
+ // reopens the byte-cap hole the removed 12000-byte throw used to close
1134
+ // (verified: commandName length 246 -> 900 bytes, 5000 -> 15,162 bytes,
1135
+ // 20000 -> 60,162 bytes — all silently over the old 12000 cap). THROW rather
1136
+ // than truncate: a truncated commandName would silently point the workflow's
1137
+ // `@~/.claude/gsd-core/commands/gsd/<stem>.md` reference at a file that does
1138
+ // not exist (see DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED) — a name
1139
+ // too long to represent is a genuine error, not something to degrade. 128 is
1140
+ // deliberately generous: the longest real shipped command name is
1141
+ // `gsd-plan-review-convergence` at 27 characters.
1142
+ const WINDSURF_COMMAND_NAME_MAX = 128;
1000
1143
  function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
1001
1144
  // #1615 security: commandName flows unsanitized into a markdown body that
1002
1145
  // Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
@@ -1005,21 +1148,43 @@ function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
1005
1148
  // Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
1006
1149
  // everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
1007
1150
  // PR #1622 security review.
1151
+ // #2931: this is a SECURITY control, not a size control — do not weaken,
1152
+ // reorder, or make it conditional on the description-truncation logic
1153
+ // added below. Keep the two concerns independent even though both happen
1154
+ // to run in this function.
1008
1155
  if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
1009
1156
  const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
1010
1157
  throw new Error(`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
1011
1158
  'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)');
1012
1159
  }
1160
+ // #2931: SEPARATE size control — see WINDSURF_COMMAND_NAME_MAX above for
1161
+ // why this exists and why it throws instead of truncating. Kept as an
1162
+ // independent check from the #1615 regex above (not folded into it, not
1163
+ // conditional on it).
1164
+ if (commandName.length > WINDSURF_COMMAND_NAME_MAX) {
1165
+ const preview = JSON.stringify(commandName.slice(0, 60));
1166
+ throw new Error(`convertClaudeCommandToWindsurfWorkflow: commandName too long (${commandName.length} chars, ` +
1167
+ `preview ${preview}...); max ${WINDSURF_COMMAND_NAME_MAX} chars (see WINDSURF_COMMAND_NAME_MAX)`);
1168
+ }
1013
1169
  const converted = convertClaudeToWindsurfMarkdown(content);
1014
1170
  const { frontmatter } = extractFrontmatterAndBody(converted);
1015
- const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
1171
+ const rawDescription = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
1172
+ // #2931: a whitespace-only description is truthy (`description || fallback`
1173
+ // would keep it) but toSingleLine() collapses it to ''. Treat it as absent
1174
+ // so the fallback is used instead of emitting a blank line.
1175
+ const singleLineDescription = rawDescription ? toSingleLine(rawDescription) : '';
1176
+ const effectiveDescription = truncateWindsurfWorkflowDescription(singleLineDescription || `Run ${commandName}.`);
1016
1177
  const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
1017
- const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
1018
- const byteLength = Buffer.byteLength(workflow, 'utf8');
1019
- if (byteLength > 12000) {
1020
- throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
1021
- }
1022
- return workflow;
1178
+ // #2931: total emission size is bounded by (fixed template text) +
1179
+ // (3 x WINDSURF_COMMAND_NAME_MAX, one per commandName/stem interpolation
1180
+ // above) + (WINDSURF_WORKFLOW_DESCRIPTION_MAX Unicode code points, up to 4
1181
+ // UTF-8 bytes each). Both inputs are validated/truncated above — commandName
1182
+ // is length-capped-and-thrown by WINDSURF_COMMAND_NAME_MAX, description is
1183
+ // truncated by truncateWindsurfWorkflowDescription — so this bound holds by
1184
+ // construction, not by measurement. The 12000-byte figure itself lives in
1185
+ // exactly one place — the cap table in tests/helpers/emitted-caps.cjs —
1186
+ // this comment only justifies why the actual emitted size stays under it.
1187
+ return `# ${commandName}\n\n${effectiveDescription}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
1023
1188
  }
1024
1189
  // --- Augment converters ---
1025
1190
  // Augment uses a tool set similar to Cursor/Windsurf.
@@ -1047,8 +1212,9 @@ function convertClaudeToAugmentMarkdown(content) {
1047
1212
  // Remove Claude Code-specific bug workarounds before brand replacement
1048
1213
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1049
1214
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1050
- // Replace "Claude Code" brand references with "Augment"
1051
- converted = converted.replace(/\bClaude Code\b/g, 'Augment');
1215
+ // Replace "Claude Code" brand references with "Augment" — #2284(b): skips
1216
+ // <runtime_compatibility> comparison-table content (protected region).
1217
+ converted = applyClaudeCodeBrandSwap(converted, 'Augment');
1052
1218
  return converted;
1053
1219
  }
1054
1220
  // #2097 (ADR-1239): command-body converters selected by descriptor
@@ -1111,10 +1277,55 @@ function convertClaudeToTraeMarkdown(content) {
1111
1277
  // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
1112
1278
  converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
1113
1279
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
1114
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
1115
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
1116
- converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
1117
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
1280
+ // #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
1281
+ // replaced before the bare Claude-instruction-file pattern and before the
1282
+ // generic dot-claude-slash rewrite below — otherwise the bare pattern
1283
+ // consumes only the instruction-filename tail, leaving that prefix stale
1284
+ // in place, and the generic rewrite then mutates the stale leftover too,
1285
+ // producing a doubled trae-prefix segment ahead of the rules path instead
1286
+ // of a single clean one. (Deliberately never spelling the instruction
1287
+ // filename as one contiguous "CLAUDE" + dot + "md" token, and never
1288
+ // spelling either malformed shape out as a literal contiguous string, in
1289
+ // ANY comment in this function: this file ships verbatim into local
1290
+ // `--trae` installs, where it is itself run through this same class of
1291
+ // find/replace — a literal instruction-filename token sitting in a
1292
+ // comment gets "fixed" right along with real code, and the emitted-content
1293
+ // regression test added alongside this fix asserts neither malformed
1294
+ // shape appears anywhere in the installed tree, comments included; this
1295
+ // bit the fix itself twice during development.) All forms converge on the
1296
+ // same concrete file (never a bare directory) so this stays in parity
1297
+ // with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
1298
+ converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1299
+ converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
1300
+ converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1301
+ converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
1302
+ // #2658 (found via the end-to-end install regression test, not the static
1303
+ // trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
1304
+ // -> runtime-config-dir rewrite on every .md file before calling this
1305
+ // converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
1306
+ // substituting a runtime-appropriate `pathPrefix` this function is never
1307
+ // given and cannot itself compute (it differs per install invocation: a
1308
+ // relative `./.trae/` for a project-local install, an arbitrary absolute
1309
+ // path for a local install rooted elsewhere, `~/.trae/` for a global one).
1310
+ // So for source using any of those prefixed forms, the patterns above
1311
+ // never fire here — this converter only ever sees the ALREADY-rewritten
1312
+ // "<runtime-config-dir>/" + instruction-filename shape, with whatever
1313
+ // prefix the install actually used. The generic pattern below preserves
1314
+ // that prefix verbatim (via the capture group) and only fixes the
1315
+ // filename suffix, rather than assuming a fixed `./.trae/` shape — a
1316
+ // narrower fixed-prefix version of this pattern shipped first and still
1317
+ // left the doubled-prefix defect live for the `$HOME/.claude/` and
1318
+ // `~/.claude/` forms specifically (found the same way, one regression-test
1319
+ // run later). Scoped to a `.trae/` tail so it cannot also swallow the
1320
+ // unprefixed `./CLAUDE.md` form the very next pattern handles differently
1321
+ // (discarding the prefix entirely, not preserving it). Must run before
1322
+ // the bare pattern for the same consume-the-full-match-first reason.
1323
+ converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
1324
+ converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
1325
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1326
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
1327
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1328
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
1118
1329
  converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
1119
1330
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
1120
1331
  converted = converted.replace(/\.claude\//g, '.trae/');
@@ -1126,7 +1337,8 @@ function convertClaudeToTraeMarkdown(content) {
1126
1337
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
1127
1338
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1128
1339
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1129
- converted = converted.replace(/\bClaude Code\b/g, 'Trae');
1340
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1341
+ converted = applyClaudeCodeBrandSwap(converted, 'Trae');
1130
1342
  return converted;
1131
1343
  }
1132
1344
  // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
@@ -1180,7 +1392,8 @@ function convertClaudeToCodebuddyMarkdown(content) {
1180
1392
  converted = converted.replace(/\.claude\//g, '.codebuddy/');
1181
1393
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1182
1394
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1183
- converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
1395
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1396
+ converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy');
1184
1397
  return converted;
1185
1398
  }
1186
1399
  function convertClaudeCommandToCodebuddySkill(content, skillName) {
@@ -1260,7 +1473,8 @@ function convertClaudeToCliineMarkdown(content) {
1260
1473
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
1261
1474
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1262
1475
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1263
- converted = converted.replace(/\bClaude Code\b/g, 'Cline');
1476
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1477
+ converted = applyClaudeCodeBrandSwap(converted, 'Cline');
1264
1478
  return converted;
1265
1479
  }
1266
1480
  /**
@@ -1412,7 +1626,9 @@ Typed mapping (agent_type-capable schema only):
1412
1626
  to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
1413
1627
  inherited, or unsupported values; do not invent one-off effort literals in
1414
1628
  workflow prose.
1415
- - \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
1629
+ - \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
1630
+ - \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
1631
+ - \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
1416
1632
  - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
1417
1633
  but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
1418
1634
  \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
@@ -1449,11 +1665,13 @@ Spawn restriction:
1449
1665
  defaulting to inline execution.
1450
1666
 
1451
1667
  Parallel fan-out:
1452
- - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
1668
+ - Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
1669
+ - Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
1453
1670
 
1454
1671
  Result parsing:
1455
1672
  - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
1456
- - \`close_agent(id)\` after collecting results from each agent
1673
+ - \`close_agent(id)\` after collecting results — but only if \`close_agent\` is visible in the current
1674
+ tool schema (check via \`tool_search\` first, same schema-detection gate as \`spawn_agent\` above)
1457
1675
  </codex_skill_adapter>`;
1458
1676
  }
1459
1677
  function convertClaudeCommandToCodexSkill(content, skillName) {
@@ -2038,8 +2256,9 @@ function convertClaudeAgentToQwenAgent(content) {
2038
2256
  let converted = content;
2039
2257
  if (_b['CLAUDE.md'])
2040
2258
  converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2259
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2041
2260
  if (_b['Claude Code'])
2042
- converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
2261
+ converted = applyClaudeCodeBrandSwap(converted, _b['Claude Code']);
2043
2262
  if (_b['.claude/'])
2044
2263
  converted = converted.replace(/\.claude\//g, _b['.claude/']);
2045
2264
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2062,6 +2281,106 @@ function convertClaudeAgentToQwenAgent(content) {
2062
2281
  fm += '---';
2063
2282
  return `${fm}\n${body}`;
2064
2283
  }
2284
+ /**
2285
+ * Convert a Claude Code agent .md for ZCode (#3384).
2286
+ *
2287
+ * ZCode is Claude-shaped (same frontmatter, same named-dispatch subagents), so
2288
+ * the file is preserved verbatim EXCEPT the `tools:` grant list: ZCode's
2289
+ * dispatcher treats every `mcp__<server>__*` entry as a REQUIRED MCP server and
2290
+ * hard-fails the subagent spawn (CONFIGURATION_ERROR: "Required MCP server is
2291
+ * not connected") whenever it is not connected, whereas Claude Code treats the
2292
+ * same entries as an optional allowlist. The `mcp__*` entries are stripped at
2293
+ * install time — the same exclusion Kimi's converter applies via
2294
+ * convertKimiToolName — so subagent spawns succeed with zero MCP servers
2295
+ * configured; connected servers' tools remain reachable (auto-discovered by the
2296
+ * host, not granted by frontmatter).
2297
+ *
2298
+ * Line-surgical by design: ONLY `tools:` lines inside the frontmatter are
2299
+ * touched, so every other byte (description, color, commented-out blocks, the
2300
+ * body) survives identically. Handles both shapes GSD emits — the inline comma
2301
+ * list (`tools: A, B, C`) and the YAML block list (`tools:` + `- A` items).
2302
+ * An agent whose filtered grant list becomes empty (every grant was `mcp__*`)
2303
+ * drops the `tools:` key entirely: an absent key inherits the full toolkit,
2304
+ * which is the degrade-gracefully outcome, never a toolless subagent.
2305
+ *
2306
+ * Byte-identical for an agent with no `mcp__*` grants (the common case) and
2307
+ * for an agent with no frontmatter at all.
2308
+ */
2309
+ function convertClaudeAgentToZcodeAgent(content) {
2310
+ // Fast path: no MCP grant token anywhere means nothing to strip. (A body
2311
+ // mention alone is not a grant — the line scan below finds no tools-line
2312
+ // change and returns `content` unchanged anyway; this just skips the scan.)
2313
+ if (!content.includes('mcp__'))
2314
+ return content;
2315
+ const lines = content.split('\n');
2316
+ if (lines[0] !== '---')
2317
+ return content;
2318
+ let fmEnd = -1;
2319
+ for (let i = 1; i < lines.length; i++) {
2320
+ if (lines[i] === '---') {
2321
+ fmEnd = i;
2322
+ break;
2323
+ }
2324
+ }
2325
+ if (fmEnd === -1)
2326
+ return content; // unterminated frontmatter — leave verbatim
2327
+ const out = [];
2328
+ let changed = false;
2329
+ let i = 1;
2330
+ while (i < fmEnd) {
2331
+ const line = lines[i];
2332
+ const inlineTools = /^tools:[ \t]*(.+)$/.exec(line);
2333
+ if (inlineTools) {
2334
+ const grants = inlineTools[1].split(',').map((tool) => tool.trim()).filter((tool) => tool !== '');
2335
+ const kept = grants.filter((tool) => !tool.startsWith('mcp__'));
2336
+ if (kept.length === grants.length) {
2337
+ out.push(line); // no mcp__* grants — keep the line byte-identical
2338
+ }
2339
+ else if (kept.length > 0) {
2340
+ out.push(`tools: ${kept.join(', ')}`);
2341
+ changed = true;
2342
+ }
2343
+ else {
2344
+ changed = true; // every grant was mcp__*: drop the tools key entirely
2345
+ }
2346
+ i++;
2347
+ continue;
2348
+ }
2349
+ if (/^tools:[ \t]*$/.test(line)) {
2350
+ // Block-list form: collect the following `- item` lines.
2351
+ const items = [];
2352
+ let j = i + 1;
2353
+ while (j < fmEnd && /^([ \t]*)-[ \t]*(\S.*)$/.test(lines[j])) {
2354
+ items.push(lines[j]);
2355
+ j++;
2356
+ }
2357
+ const kept = items.filter((item) => {
2358
+ const name = /^([ \t]*)-[ \t]*(\S.*)$/.exec(item)[2].trim();
2359
+ return !name.startsWith('mcp__');
2360
+ });
2361
+ if (kept.length !== items.length) {
2362
+ changed = true;
2363
+ if (kept.length > 0) {
2364
+ out.push(line);
2365
+ out.push(...kept);
2366
+ } // else: drop the tools key and all its items
2367
+ }
2368
+ else {
2369
+ out.push(line, ...items);
2370
+ }
2371
+ i = j;
2372
+ continue;
2373
+ }
2374
+ out.push(line);
2375
+ i++;
2376
+ }
2377
+ if (!changed)
2378
+ return content;
2379
+ // Opening delimiter + transformed frontmatter + closing delimiter + body.
2380
+ out.unshift(lines[0]);
2381
+ out.push(...lines.slice(fmEnd));
2382
+ return out.join('\n');
2383
+ }
2065
2384
  function convertClaudeAgentToCodebuddyAgent(content) {
2066
2385
  const converted = convertClaudeToCodebuddyMarkdown(content);
2067
2386
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2082,6 +2401,48 @@ function convertClaudeAgentToClineAgent(content) {
2082
2401
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
2083
2402
  return `${cleanFrontmatter}\n${body}`;
2084
2403
  }
2404
+ /**
2405
+ * Apply a runtime's descriptor-declared `hostBehaviors.brandingRewrites` to an
2406
+ * agent body — the three literal-substring replaces the inline agent loop
2407
+ * (bin/install.js) previously hardcoded per-branding-runtime (qwen/hermes):
2408
+ * CLAUDE.md -> brandingRewrites['CLAUDE.md']
2409
+ * Claude Code -> brandingRewrites['Claude Code'] (word-boundary, \bClaude Code\b)
2410
+ * .claude/ -> brandingRewrites['.claude/']
2411
+ *
2412
+ * Data-driven (#2875 Part 2 / J10): reads the rewrite table from the
2413
+ * runtime's OWN descriptor rather than hardcoding any runtime's strings, so a
2414
+ * runtime declaring a different `brandingRewrites` table gets its own
2415
+ * rewrites applied automatically. A runtime with no `brandingRewrites`
2416
+ * declared returns `content` unchanged (no rewrite table to apply).
2417
+ *
2418
+ * Byte-identical to the inline loop's `else if (_hostBehaviors(runtime).brandingRewrites)`
2419
+ * branch, including plain (non-word-boundary) `.replace(/\bClaude Code\b/g, ...)`
2420
+ * semantics — J9.
2421
+ */
2422
+ function applyAgentBrandingRewrites(content, runtime) {
2423
+ const _b = _hostBehaviors(runtime).brandingRewrites;
2424
+ if (!_b)
2425
+ return content;
2426
+ let converted = content;
2427
+ if (_b['CLAUDE.md'])
2428
+ converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2429
+ if (_b['Claude Code'])
2430
+ converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
2431
+ if (_b['.claude/'])
2432
+ converted = converted.replace(/\.claude\//g, _b['.claude/']);
2433
+ return converted;
2434
+ }
2435
+ /**
2436
+ * Named branding converter for Hermes agents (#2875 Part 2 / J9-J10).
2437
+ * `convertedAgentsKind` dispatches converters by exported name, so a named
2438
+ * export is required even though the transform itself is fully generic
2439
+ * (`applyAgentBrandingRewrites`) — resolved from
2440
+ * `capabilities/hermes/capability.json`'s `hostBehaviors.brandingRewrites`,
2441
+ * never hardcoded here.
2442
+ */
2443
+ function convertClaudeAgentToHermesAgent(content) {
2444
+ return applyAgentBrandingRewrites(content, 'hermes');
2445
+ }
2085
2446
  /**
2086
2447
  * Convert Claude Code agent markdown to Codex agent format.
2087
2448
  * Applies base markdown conversions, then adds a <codex_agent_role> header
@@ -2194,20 +2555,145 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
2194
2555
  const NON_CLAUDE_RUNTIMES = Object.keys(capabilityRegistry.runtimes)
2195
2556
  .filter((id) => id !== 'claude')
2196
2557
  .sort();
2558
+ /**
2559
+ * #2652: The isolation a runtime can actually negotiate at dispatch time,
2560
+ * resolved from the registry exactly as `gsd_run query dispatch-isolation`
2561
+ * resolves it at runtime (`routeDispatchIsolation`, gsd-core/bin/gsd-tools.cjs):
2562
+ * the declared value must be in the closed vocabulary, a `harness-worktree`
2563
+ * host must also declare the flag the scheduler passes, and an
2564
+ * `orchestrator-worktree` host must carry a descriptor that resolves. Anything
2565
+ * else — unknown runtime, `undocumented`, out-of-vocabulary, a throw — is
2566
+ * `none` (ADR-1239, "Fail-closed").
2567
+ *
2568
+ * Install time cannot know the worktree path a future dispatch will target, so
2569
+ * the descriptor is probed with a placeholder; `resolveOrchestratorExec` fails
2570
+ * only on descriptor shape, never on a well-formed target's value.
2571
+ *
2572
+ * @private — exported as `_negotiatedDispatchIsolation` for tests.
2573
+ */
2574
+ function _negotiatedDispatchIsolation(runtime) {
2575
+ try {
2576
+ const runtimeEntry = capabilityRegistry?.runtimes?.[runtime] ?? null;
2577
+ const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null;
2578
+ if (declared === 'harness-worktree') {
2579
+ const declaredFlag = runtimeEntry?.runtime?.harnessIsolationFlag ?? null;
2580
+ return typeof declaredFlag === 'string' && declaredFlag.length > 0
2581
+ ? 'harness-worktree'
2582
+ : 'none';
2583
+ }
2584
+ if (declared === 'orchestrator-worktree') {
2585
+ return hostIntegration.resolveOrchestratorExec(runtimeEntry?.runtime?.orchestratorExec, '/gsd-orchestrator-worktree-probe').ok
2586
+ ? 'orchestrator-worktree'
2587
+ : 'none';
2588
+ }
2589
+ return 'none';
2590
+ }
2591
+ catch {
2592
+ return 'none';
2593
+ }
2594
+ }
2197
2595
  /**
2198
2596
  * #1521: Every non-Claude runtime resolves its own runtime identity from a
2199
- * runtime-neutral config, and defaults workflow.use_worktrees to false —
2200
- * GSD's worktree isolation uses Claude Code's isolation="worktree" spawn
2201
- * parameter, which no other runtime honors. Stamped into the emitted
2202
- * workflow runtime-resolution blocks. (Generalizes the Codex-only #1515 fix.)
2597
+ * runtime-neutral config. Stamped into the emitted workflow runtime-resolution
2598
+ * blocks. (Generalizes the Codex-only #1515 fix.)
2599
+ *
2600
+ * #1521 also stamped `workflow.use_worktrees` to default false for every
2601
+ * non-Claude runtime, because GSD's worktree isolation was Claude Code's
2602
+ * `isolation="worktree"` spawn parameter and no other runtime honored it.
2603
+ * #2584 removed that premise: isolation is now a negotiated capability
2604
+ * (`dispatch.isolation`), and Cursor declares `harness-worktree` while Codex,
2605
+ * OpenCode, Kimi and Kimi Code declare `orchestrator-worktree`. Stamping the
2606
+ * false default for those hosts resolved `USE_WORKTREES=false` before
2607
+ * `dispatch.isolation` was ever consulted, so a runtime that declares worktree
2608
+ * support still got `ISOLATION=none` — judged by its name after all, which is
2609
+ * the defect #2652 exists to remove. The stamp is therefore scoped to the
2610
+ * runtimes whose negotiated isolation really is `none`, where the default it
2611
+ * writes is the outcome the resolver would reach anyway.
2203
2612
  *
2204
2613
  * @private — exported as `_stampNonClaudeRuntimeDefaults` for tests.
2205
2614
  */
2206
2615
  function _stampNonClaudeRuntimeDefaults(content, runtime) {
2207
- content = content.replace(/config-get workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo "true"/g, 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"');
2616
+ if (_negotiatedDispatchIsolation(runtime) === 'none') {
2617
+ content = content.replace(/config-get workflow\.use_worktrees --raw 2>\/dev\/null \|\| echo "true"/g, 'config-get workflow.use_worktrees --default false --raw 2>/dev/null || echo "false"');
2618
+ }
2208
2619
  content = content.replace(/config-get runtime --default claude --raw 2>\/dev\/null \|\| echo "claude"/g, `config-get runtime --default ${runtime} --raw 2>/dev/null || echo "${runtime}"`);
2209
2620
  return content;
2210
2621
  }
2622
+ /**
2623
+ * #3544 (extending #3133's fix): restore `@$HOME<suffix>` `@`-file-reference
2624
+ * lines back to their tilde equivalent (`@~<suffix>`) in Claude-emitted
2625
+ * content whose pathPrefix is the `$HOME` form. This is a NARROW,
2626
+ * context-sensitive correction layered on top of the blanket `~/.claude/` /
2627
+ * `$HOME/.claude/` -> pathPrefix substitution every Claude emit path
2628
+ * applies: that blanket substitution MUST keep emitting `$HOME` for global
2629
+ * installs — shell commands embedded in workflow/command bodies (e.g.
2630
+ * `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs"`) need it, since `~` does
2631
+ * not expand inside double-quoted shell strings (#1284). But Claude Code's
2632
+ * own `@`-import resolver does the opposite: it documents `~` expansion and
2633
+ * does NOT expand `$HOME`. That is not merely undocumented — a controlled
2634
+ * `/context` measurement showed an `@$HOME/…` import loading nothing (see
2635
+ * .gsd/bug/fix-3544-home-expansion-spec-tree/10-diagnosis.md's ADDENDUM). No
2636
+ * automated test can verify *resolution* inside a live Claude Code session
2637
+ * (nothing in CI can spawn one and read `/context`); every test here — unit
2638
+ * and spawned-installer alike — verifies only the emitted STRING takes the
2639
+ * `~` form Claude Code documents as expanding. A single pathPrefix string
2640
+ * cannot satisfy both the shell and the `@`-import consumer, so this runs as
2641
+ * a second, `@`-anchored pass AFTER the blanket substitution.
2642
+ *
2643
+ * #3133 first applied this restore inline in `_applyRuntimeRewrites`'s
2644
+ * `case 'claude'` below (the skill/command staging pipeline). #3544 found
2645
+ * the identical defect in bin/install.js's `copyWithPathReplacement` — the
2646
+ * `gsd-core/` spec-tree emit path, which never had the restore step, so
2647
+ * every `@~/.claude/gsd-core/…` include in a global install's workflows/
2648
+ * references tree silently resolved to nothing (54 includes across 22 files
2649
+ * on a live install, per the diagnosis). Both call sites now share this one
2650
+ * implementation instead of drifting independently (DEFECT.GENERATIVE-FIX).
2651
+ *
2652
+ * No-op unless `pathPrefix` is the `$HOME` form — local installs already
2653
+ * bake an absolute, `@`-resolvable pathPrefix and are unaffected, as are
2654
+ * every non-Claude runtime (never called for them).
2655
+ *
2656
+ * #3544 review (2nd pass): the first cut of this function hardcoded the
2657
+ * literal `.claude/` segment, so it silently no-opped for any global install
2658
+ * under a non-default `--config-dir` (e.g. `~/.claude-work`) — reproducing
2659
+ * the exact defect #3544 fixes, just one directory name later. This ALSO
2660
+ * corrects the same latent gap in #3133's original path, since both call
2661
+ * sites share this one implementation. Fixed by deriving the rewrite from
2662
+ * `pathPrefix` itself rather than a hardcoded directory name: the tilde
2663
+ * equivalent of any `$HOME`-form prefix is `'~' + pathPrefix.slice(5)`
2664
+ * (`'$HOME'.length === 5`), so the transform generalizes to any config-dir
2665
+ * name with no runtime-specific literal.
2666
+ *
2667
+ * #3544 review (2nd pass), quote-awareness: the anchor is a negative
2668
+ * lookbehind for a preceding quote character, NOT a line-start anchor —
2669
+ * Claude Code documents `@`-references as valid "anywhere in your
2670
+ * CLAUDE.md" (e.g. `See @README for project overview`), so anchoring to
2671
+ * line-start would miss a legitimate mid-line reference. The lookbehind
2672
+ * instead guards the one demonstrated false-positive: a quoted shell string
2673
+ * like `echo "@$HOME/.claude/x"`, where rewriting `$HOME` to `~` inside
2674
+ * double quotes reintroduces the #1284 failure mode (`~` does not expand in
2675
+ * double-quoted shell). Deliberately NOT fenced-code-block aware (unlike
2676
+ * `resolveSpecRootReference`'s `scanFencedBlocks` use above): this pass
2677
+ * targets genuine `@`-import lines and inline shell references across the
2678
+ * whole emitted corpus, and today there are zero occurrences anywhere in the
2679
+ * tree of an `@$HOME<suffix>` sequence inside a fenced code block (the
2680
+ * quote-guard already closes the one reachable false-positive class).
2681
+ * Layering `scanFencedBlocks` on top would roughly double this function's
2682
+ * size to guard an undemonstrated case — the opposite of the brief's
2683
+ * "simpler, not more complex" direction. If a fenced example ever needs this
2684
+ * literal sequence, add fence-awareness then, with a regression test proving
2685
+ * the fence is real.
2686
+ *
2687
+ * @private — exported as `_restoreClaudeGlobalAtRefTilde` for tests and for
2688
+ * bin/install.js's `copyWithPathReplacement`.
2689
+ */
2690
+ function restoreClaudeGlobalAtRefTilde(content, pathPrefix) {
2691
+ if (typeof pathPrefix !== 'string' || !pathPrefix.startsWith('$HOME'))
2692
+ return content;
2693
+ const tildeEquivalent = '~' + pathPrefix.slice('$HOME'.length);
2694
+ const atRefRe = new RegExp(`(?<!["'])@${(0, pattern_cjs_1.escapeRegex)(pathPrefix)}`, 'g');
2695
+ return content.replace(atRefRe, `@${tildeEquivalent}`);
2696
+ }
2211
2697
  /**
2212
2698
  * Apply the per-runtime rewrite table to a single content string.
2213
2699
  * Relocated from bin/install.js `_applyRuntimeRewrites`.
@@ -2284,7 +2770,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2284
2770
  // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
2285
2771
  // dirName-derived (no runtime literal). getDirName('augment') resolves
2286
2772
  // to '.augment', so this is byte-identical to the prior hardcoded regexes.
2287
- const _dd = escapeRegExp(dirName);
2773
+ const _dd = (0, pattern_cjs_1.escapeRegex)(dirName);
2288
2774
  content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
2289
2775
  content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
2290
2776
  content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
@@ -2302,7 +2788,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2302
2788
  // #2094: descriptor-driven — dirName resolves to '.trae' via
2303
2789
  // getDirName()/localConfigDir, so this regex is built rather than
2304
2790
  // hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
2305
- content = content.replace(new RegExp('~/' + escapeRegExp(dirName) + '/', 'g'), pathPrefix);
2791
+ content = content.replace(new RegExp('~/' + (0, pattern_cjs_1.escapeRegex)(dirName) + '/', 'g'), pathPrefix);
2306
2792
  content = processAttribution(content, attribution);
2307
2793
  break;
2308
2794
  case 'codebuddy':
@@ -2328,6 +2814,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2328
2814
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2329
2815
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2330
2816
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2817
+ // #3133 / #3544: restore @-file-reference lines to the tilde form
2818
+ // Claude actually expands — see restoreClaudeGlobalAtRefTilde's doc
2819
+ // comment above for why this must be a separate, @-anchored pass.
2820
+ content = restoreClaudeGlobalAtRefTilde(content, pathPrefix);
2331
2821
  content = processAttribution(content, attribution);
2332
2822
  break;
2333
2823
  // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
@@ -2342,7 +2832,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2342
2832
  const _b = _hostBehaviors(runtime).brandingRewrites;
2343
2833
  if (_b) {
2344
2834
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2345
- content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2835
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2836
+ content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
2346
2837
  }
2347
2838
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2348
2839
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2366,7 +2857,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2366
2857
  const _b = _hostBehaviors(runtime).brandingRewrites;
2367
2858
  if (_b) {
2368
2859
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2369
- content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2860
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2861
+ content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
2370
2862
  }
2371
2863
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2372
2864
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2413,18 +2905,18 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2413
2905
  * @param attribution Co-Authored-By value (string | null | undefined)
2414
2906
  */
2415
2907
  function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2416
- if (!node_fs_1.default.existsSync(stagedDir))
2908
+ if (!installFs().existsSync(stagedDir))
2417
2909
  return;
2418
2910
  const walkAndRewrite = (dir) => {
2419
- for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
2911
+ for (const entry of installFs().readdirSync(dir, { withFileTypes: true })) {
2420
2912
  const fullPath = node_path_1.default.join(dir, entry.name);
2421
2913
  if (entry.isDirectory()) {
2422
2914
  walkAndRewrite(fullPath);
2423
2915
  }
2424
2916
  else if (entry.name.endsWith('.md')) {
2425
- let content = node_fs_1.default.readFileSync(fullPath, 'utf8');
2917
+ let content = installFs().readFileSync(fullPath, 'utf8');
2426
2918
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2427
- node_fs_1.default.writeFileSync(fullPath, content);
2919
+ installFs().writeFileSync(fullPath, content);
2428
2920
  }
2429
2921
  }
2430
2922
  };
@@ -2449,14 +2941,14 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
2449
2941
  * @returns {string} path to the temp dir (caller is responsible for cleanup)
2450
2942
  */
2451
2943
  function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2452
- if (!node_fs_1.default.existsSync(stagedDir))
2944
+ if (!installFs().existsSync(stagedDir))
2453
2945
  return stagedDir;
2454
- const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-cmd-rewrites-'));
2946
+ const tempDir = mkInstallTempDir('gsd-cmd-rewrites-');
2455
2947
  try {
2456
- for (const entry of node_fs_1.default.readdirSync(stagedDir, { withFileTypes: true })) {
2948
+ for (const entry of installFs().readdirSync(stagedDir, { withFileTypes: true })) {
2457
2949
  if (!entry.isFile() || !entry.name.endsWith('.md'))
2458
2950
  continue;
2459
- let content = node_fs_1.default.readFileSync(node_path_1.default.join(stagedDir, entry.name), 'utf8');
2951
+ let content = installFs().readFileSync(node_path_1.default.join(stagedDir, entry.name), 'utf8');
2460
2952
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2461
2953
  // #2097 (ADR-1239): descriptor-driven — commandBodyConverter name comes
2462
2954
  // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
@@ -2464,18 +2956,52 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2464
2956
  if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
2465
2957
  content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
2466
2958
  }
2467
- node_fs_1.default.writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
2959
+ installFs().writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
2468
2960
  }
2469
2961
  }
2470
2962
  catch (err) {
2471
2963
  try {
2472
- node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
2964
+ installFs().rmSync(tempDir, { recursive: true, force: true });
2473
2965
  }
2474
2966
  catch { /* best-effort */ }
2475
2967
  throw err;
2476
2968
  }
2477
2969
  return tempDir;
2478
2970
  }
2971
+ /**
2972
+ * #2873 (4b) — second pass over a staged skills directory, run strictly AFTER
2973
+ * `applyRuntimeContentRewritesInPlace`. That pass's `case 'claude':` branch
2974
+ * unconditionally rewrites any bare (non-`@`-prefixed) `~/.claude/` substring
2975
+ * in the body to the computed pathPrefix (`$HOME/.claude/` for a global
2976
+ * install) and restores ONLY the `@`-prefixed form back to `~`
2977
+ * (`@$HOME/.claude/` → `@~/.claude/`). `resolveSpecRootReference`'s
2978
+ * replacement text is deliberately imperative prose containing a literal,
2979
+ * non-`@`-prefixed `~/.claude/gsd-core/workflows/<stem>.md` — running it
2980
+ * BEFORE the pass above would let that literal tilde text get silently
2981
+ * mangled into the undocumented `$HOME/` form the design explicitly rejects.
2982
+ * Running it here, after, means it only ever sees the FINAL
2983
+ * `@~/.claude/gsd-core/workflows/<stem>.md` include line (which survives the
2984
+ * pass above intact via its own `@`-guarded restore).
2985
+ */
2986
+ function applySpecRootReferenceToStagedSkills(stagedDir) {
2987
+ if (!installFs().existsSync(stagedDir))
2988
+ return;
2989
+ const walk = (dir) => {
2990
+ for (const entry of installFs().readdirSync(dir, { withFileTypes: true })) {
2991
+ const fullPath = node_path_1.default.join(dir, entry.name);
2992
+ if (entry.isDirectory()) {
2993
+ walk(fullPath);
2994
+ }
2995
+ else if (entry.name === 'SKILL.md') {
2996
+ const content = installFs().readFileSync(fullPath, 'utf8');
2997
+ const rewritten = resolveSpecRootReference(content);
2998
+ if (rewritten !== content)
2999
+ installFs().writeFileSync(fullPath, rewritten);
3000
+ }
3001
+ }
3002
+ };
3003
+ walk(stagedDir);
3004
+ }
2479
3005
  /**
2480
3006
  * HIGH-LEVEL: In-place fs walk: rewrite all .md files under stagedDir for the given runtime.
2481
3007
  *
@@ -2492,16 +3018,30 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2492
3018
  */
2493
3019
  function rewriteStagedSkillBodies(stagedDir, opts) {
2494
3020
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2495
- if (!node_fs_1.default.existsSync(stagedDir))
3021
+ if (!installFs().existsSync(stagedDir))
2496
3022
  return;
2497
3023
  const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2498
3024
  const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2499
- const isGlobal = scope === 'global';
3025
+ // #2870: `scope` is defaulted to 'global' above, so it is never undefined
3026
+ // here, and every reachable caller passes 'global' | 'local' | undefined —
3027
+ // isGlobalScope's throw-on-out-of-union case is unreachable at this site.
3028
+ const isGlobal = (0, install_scope_cjs_1.isGlobalScope)(scope);
2500
3029
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2501
3030
  const isWindowsHost = platform === 'win32';
2502
3031
  const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
2503
3032
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2504
3033
  applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
3034
+ // #2873 (4b): claude, global scope only — see
3035
+ // applySpecRootReferenceToStagedSkills's doc comment for why this MUST run
3036
+ // after the rewrite pass above, not before. `rewriteStagedSkillBodies` is
3037
+ // the skills-kind seam (`kind.kind === 'skills'`), so this never touches a
3038
+ // 'commands' or 'agents' kind body (rows 24/25 unaffected), and claude has
3039
+ // no skills-kind entry at local scope, so this is already structurally
3040
+ // scoped to global (row 23) — the explicit isGlobal check is defense-in-depth
3041
+ // against that descriptor wiring ever changing.
3042
+ if (runtime === 'claude' && isGlobal) {
3043
+ applySpecRootReferenceToStagedSkills(stagedDir);
3044
+ }
2505
3045
  }
2506
3046
  /**
2507
3047
  * HIGH-LEVEL: Copy-to-temp then rewrite all .md files for the given runtime.
@@ -2521,11 +3061,14 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
2521
3061
  */
2522
3062
  function rewriteStagedCommandBodies(stagedDir, opts) {
2523
3063
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2524
- if (!node_fs_1.default.existsSync(stagedDir))
3064
+ if (!installFs().existsSync(stagedDir))
2525
3065
  return stagedDir;
2526
3066
  const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2527
3067
  const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2528
- const isGlobal = scope === 'global';
3068
+ // #2870: `scope` is defaulted to 'global' above, so it is never undefined
3069
+ // here, and every reachable caller passes 'global' | 'local' | undefined —
3070
+ // isGlobalScope's throw-on-out-of-union case is unreachable at this site.
3071
+ const isGlobal = (0, install_scope_cjs_1.isGlobalScope)(scope);
2529
3072
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2530
3073
  const isWindowsHost = platform === 'win32';
2531
3074
  const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
@@ -2586,6 +3129,127 @@ function applyAgentPathRewrites(content, runtime, pathPrefix) {
2586
3129
  return content;
2587
3130
  }
2588
3131
  // ── End rewrite engine ────────────────────────────────────────────────────────
3132
+ /**
3133
+ * Derive an agent's stem name from its source `.md` filename. Byte-identical
3134
+ * to the inline agent loop's `entry.name.replace(/\.md$/, '')` (bin/install.js)
3135
+ * — single-sourced here so the descriptor pipeline's per-agent resolution
3136
+ * context (`agentCtx.agentName`, ADR-1235 §1 / #2875 Part 2 row I3) can never
3137
+ * diverge from it. A filename with no trailing `.md` is returned unchanged
3138
+ * (the regex has nothing to match) — I3's boundary row.
3139
+ */
3140
+ function deriveAgentName(fileName) {
3141
+ return fileName.replace(/\.md$/, '');
3142
+ }
3143
+ /**
3144
+ * #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
3145
+ * file in a newline-agnostic way (LF and CRLF source files are both handled).
3146
+ * Relocated verbatim from bin/install.js (#2875 Part 2) — see
3147
+ * `applyAgentFrontmatterExtensions` below for the orchestration that calls it.
3148
+ *
3149
+ * The function:
3150
+ * - Detects the file's EOL (CRLF if the first `---` line ends with \r\n,
3151
+ * otherwise LF).
3152
+ * - Skips injection if an `effort:` key already exists in the frontmatter
3153
+ * (idempotent).
3154
+ * - Inserts `effort: <value>` immediately before the closing `---` delimiter,
3155
+ * using the same EOL as the surrounding frontmatter so the output file
3156
+ * stays EOL-consistent.
3157
+ * - Returns the original content unchanged when no YAML frontmatter is found.
3158
+ */
3159
+ function injectEffortFrontmatter(content, effortValue) {
3160
+ const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
3161
+ const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
3162
+ const match = fmRe.exec(content);
3163
+ if (!match)
3164
+ return content; // no YAML frontmatter — leave unchanged
3165
+ const fmBody = match[1]; // content between the two `---` lines
3166
+ if (/^effort:/m.test(fmBody))
3167
+ return content;
3168
+ const openLen = 3 + eol.length; // "---" + eol
3169
+ const closingStart = match.index + openLen + fmBody.length;
3170
+ const before = content.slice(0, closingStart);
3171
+ const after = content.slice(closingStart);
3172
+ return `${before}effort: ${effortValue}${eol}${after}`;
3173
+ }
3174
+ /**
3175
+ * #767 — Inject `disallowedTools: <value>` into the YAML frontmatter of a
3176
+ * Claude .md agent. Mirrors injectEffortFrontmatter: idempotent (skips if
3177
+ * disallowedTools: already present), inserts immediately before the closing
3178
+ * `---`. Claude-only — never call for other runtimes, which break on unknown
3179
+ * frontmatter keys. Relocated verbatim from bin/install.js (#2875 Part 2).
3180
+ */
3181
+ function injectDisallowedToolsFrontmatter(content, disallowedValue) {
3182
+ const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
3183
+ const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
3184
+ const match = fmRe.exec(content);
3185
+ if (!match)
3186
+ return content; // no YAML frontmatter — leave unchanged
3187
+ const fmBody = match[1]; // content between the two `---` lines
3188
+ if (/^disallowedTools:/m.test(fmBody))
3189
+ return content;
3190
+ const openLen = 3 + eol.length; // "---" + eol
3191
+ const closingStart = match.index + openLen + fmBody.length;
3192
+ const before = content.slice(0, closingStart);
3193
+ const after = content.slice(closingStart);
3194
+ return `${before}disallowedTools: ${disallowedValue}${eol}${after}`;
3195
+ }
3196
+ // #767 — Read-only verifier/auditor agents get a Claude-Code disallowedTools deny-list.
3197
+ // Group A (pure read-only) deny Write,Edit,MultiEdit. Group B report-writers Write one
3198
+ // output file so they deny only Edit,MultiEdit. gsd-nyquist-auditor is intentionally
3199
+ // excluded (it legitimately uses Write AND Edit to create/patch test files). Relocated
3200
+ // verbatim from bin/install.js (#2875 Part 2) — single source of truth for both the
3201
+ // inline loop (which now requires this export) and the descriptor pipeline.
3202
+ const READONLY_AGENT_DISALLOWED_TOOLS = {
3203
+ 'gsd-plan-checker': 'Write, Edit, MultiEdit',
3204
+ 'gsd-integration-checker': 'Write, Edit, MultiEdit',
3205
+ 'gsd-ui-checker': 'Write, Edit, MultiEdit',
3206
+ 'gsd-verifier': 'Edit, MultiEdit',
3207
+ 'gsd-doc-verifier': 'Edit, MultiEdit',
3208
+ 'gsd-eval-auditor': 'Edit, MultiEdit',
3209
+ 'gsd-ui-auditor': 'Edit, MultiEdit',
3210
+ };
3211
+ /**
3212
+ * Post-converter frontmatter-extensions step (#2875 Part 2 / ADR-1235 §1
3213
+ * follow-up). Driven by the runtime descriptor's
3214
+ * `hostBehaviors.agentFrontmatterExtensions` allow-list — Claude is its only
3215
+ * declared consumer today (`agentFrontmatterExtensions: ["effort"]`).
3216
+ * A runtime that does NOT declare the extension gets nothing injected (J3):
3217
+ * OpenCode/Qwen/Hermes reject unknown frontmatter keys.
3218
+ *
3219
+ * Byte-identical to the inline agent loop's
3220
+ * `if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort'))`
3221
+ * block (bin/install.js): both the effort injection AND the disallowedTools
3222
+ * injection are gated behind the SAME `'effort'` extension flag — there is no
3223
+ * separate `'disallowedTools'` extension key, mirroring the loop exactly.
3224
+ *
3225
+ * J2 (the trap row): when the resolved effort is `'inherit'`, NO `effort:`
3226
+ * key is written at all — the absence of the key IS the behavior (#3533).
3227
+ * Writing `effort: inherit` would be a regression that looks like success.
3228
+ *
3229
+ * @param content agent .md content, already converter-transformed
3230
+ * @param runtime canonical runtime ID
3231
+ * @param agentName agent stem (from deriveAgentName), e.g. 'gsd-planner'
3232
+ * @param targetDir install root — resolves .planning/config.json + ~/.gsd/defaults.json
3233
+ */
3234
+ function applyAgentFrontmatterExtensions(content, { runtime, agentName, targetDir }) {
3235
+ const extensions = _hostBehaviors(runtime).agentFrontmatterExtensions || [];
3236
+ if (!extensions.includes('effort'))
3237
+ return content;
3238
+ let result = content;
3239
+ const effortCfg = readGsdEffectiveEffortConfig(targetDir ?? null);
3240
+ const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
3241
+ // #3533 (10d): 'inherit' means the effort: key must NOT exist — Claude Code
3242
+ // then follows the session effort. The canonical source agents carry no
3243
+ // effort key, so skipping injection is the whole job.
3244
+ if (universalEffort !== 'inherit') {
3245
+ const renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, universalEffort).value;
3246
+ result = injectEffortFrontmatter(result, renderedEffort);
3247
+ }
3248
+ const disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[agentName];
3249
+ if (disallowedTools)
3250
+ result = injectDisallowedToolsFrontmatter(result, disallowedTools);
3251
+ return result;
3252
+ }
2589
3253
  /**
2590
3254
  * Apply Co-Authored-By attribution policy to file content.
2591
3255
  * - null -> remove the Co-Authored-By line and its preceding blank line
@@ -2628,15 +3292,25 @@ module.exports = {
2628
3292
  convertClaudeToAntigravityContent,
2629
3293
  convertClaudeCommandToAntigravitySkill,
2630
3294
  convertClaudeCommandToClaudeSkill,
3295
+ // #2873 (4b): pure, scope-free transform — applied by the one call site
3296
+ // that knows install scope (skillsKind's stage() in
3297
+ // runtime-artifact-layout.cts), never inside convertClaudeCommandToClaudeSkill
3298
+ // itself.
3299
+ resolveSpecRootReference,
2631
3300
  convertClaudeCommandToKimiSkill,
2632
3301
  convertClaudeCommandToKimiCodeSkill,
2633
3302
  buildKimiAgentArtifacts,
2634
3303
  convertClaudeToCursorMarkdown,
2635
3304
  convertClaudeCommandToCursorSkill,
2636
- convertClaudeCommandToCursorCommand,
2637
3305
  convertClaudeToWindsurfMarkdown,
2638
3306
  convertClaudeCommandToWindsurfSkill,
2639
3307
  convertClaudeCommandToWindsurfWorkflow,
3308
+ // #2931: single-sourced brand-swap helper (was duplicated verbatim in
3309
+ // bin/install.js — the exact drift class this PR exists to reduce). Used
3310
+ // internally by convertClaudeToWindsurfMarkdown/convertClaudeToAugmentMarkdown
3311
+ // above and bound from here by the remaining bin/install.js converters
3312
+ // (Cursor/Trae/CodeBuddy/Cline) that still brand-swap inline.
3313
+ applyClaudeCodeBrandSwap,
2640
3314
  convertClaudeToAugmentMarkdown,
2641
3315
  convertClaudeCommandToAugmentSkill,
2642
3316
  convertClaudeToTraeMarkdown,
@@ -2677,11 +3351,20 @@ module.exports = {
2677
3351
  convertClaudeAgentToCodebuddyAgent,
2678
3352
  convertClaudeAgentToClineAgent,
2679
3353
  convertClaudeAgentToCodexAgent,
3354
+ // #2875 Part 2 (J10): Hermes named branding converter, generic underlying
3355
+ // transform exported alongside it for direct reuse/testing.
3356
+ convertClaudeAgentToHermesAgent,
3357
+ applyAgentBrandingRewrites,
2680
3358
  // ADR-1239 / #2092 Phase B Upgrade 1: native .qwen/agents/*.md subagent
2681
3359
  // projection — registered by name so convertedAgentsKind's
2682
3360
  // conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
2683
3361
  // can resolve it from capabilities/qwen/capability.json's agents kind.
2684
3362
  convertClaudeAgentToQwenAgent,
3363
+ // #3384: ZCode agents are Claude-shaped but its dispatcher treats mcp__*
3364
+ // tools grants as required MCP servers — registered by name for the same
3365
+ // conversionExports[converterName] dispatch, resolved from
3366
+ // capabilities/zcode/capability.json's agents kind.
3367
+ convertClaudeAgentToZcodeAgent,
2685
3368
  // #1511 ADR-1508 Phase 2: rewrite engine deep seam
2686
3369
  // Low-level walkers (pathPrefix + attribution pre-resolved by caller):
2687
3370
  applyRuntimeContentRewritesInPlace,
@@ -2692,9 +3375,21 @@ module.exports = {
2692
3375
  // ADR-1235 §1: descriptor-driven agent cross-cutting
2693
3376
  applyAgentPathRewrites,
2694
3377
  normalizeAgentBodyForRuntime,
3378
+ // #2875 Part 2: descriptor-driven agent frontmatter-extensions step + its
3379
+ // single-sourced building blocks (also required back by bin/install.js so
3380
+ // the inline loop and the descriptor pipeline resolve through the SAME
3381
+ // code — no drift between the two byte-parity-gated pipelines).
3382
+ deriveAgentName,
3383
+ injectEffortFrontmatter,
3384
+ injectDisallowedToolsFrontmatter,
3385
+ READONLY_AGENT_DISALLOWED_TOOLS,
3386
+ applyAgentFrontmatterExtensions,
2695
3387
  _computePathPrefix: computePathPrefix,
3388
+ _restoreClaudeGlobalAtRefTilde: restoreClaudeGlobalAtRefTilde,
2696
3389
  _applyRuntimeRewrites,
2697
3390
  _stampNonClaudeRuntimeDefaults,
3391
+ // #2652: registry-resolved dispatch isolation, mirroring routeDispatchIsolation
3392
+ _negotiatedDispatchIsolation,
2698
3393
  // #1521: canonical non-Claude runtime list for test files and tooling
2699
3394
  NON_CLAUDE_RUNTIMES,
2700
3395
  };