@opengsd/gsd-core 1.10.0 → 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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -21,12 +21,34 @@ 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
@@ -270,11 +292,8 @@ function buildKiloAgentPermissionBlock(claudeTools) {
270
292
  }
271
293
  return lines;
272
294
  }
273
- function escapeRegExp(value) {
274
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
275
- }
276
295
  function replaceRelativePathReference(content, fromPath, toPath) {
277
- const escapedPath = escapeRegExp(fromPath);
296
+ const escapedPath = (0, pattern_cjs_1.escapeRegex)(fromPath);
278
297
  return content.replace(new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'), (_, prefix) => `${prefix}${toPath}`);
279
298
  }
280
299
  /**
@@ -367,16 +386,6 @@ function skillFrontmatterName(skillDirName) {
367
386
  // Return the hyphen form as-is (gsd-<cmd>) — canonical since #2808.
368
387
  return skillDirName;
369
388
  }
370
- function normalizeClaudeSkillEffort(effort) {
371
- // #3039: `max` is rejected by Anthropic models when extended thinking is
372
- // disabled (400: output_config.effort 'max' is not supported when thinking
373
- // is disabled). The frontmatter is static at install time and the installer
374
- // cannot know whether thinking will be on or off at invocation. `high` is the
375
- // maximum value that works in both states on all supported models.
376
- if (effort === 'xhigh' || effort === 'max')
377
- return 'high';
378
- return effort;
379
- }
380
389
  /**
381
390
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
382
391
  * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -431,10 +440,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
431
440
  const description = extractFrontmatterField(frontmatter, 'description') || '';
432
441
  const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
433
442
  const agent = extractFrontmatterField(frontmatter, 'agent');
434
- // #769: preserve context: and effort: from source command files so they
435
- // 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.)
436
449
  const context = extractFrontmatterField(frontmatter, 'context');
437
- const effort = extractFrontmatterField(frontmatter, 'effort');
438
450
  // Preserve allowed-tools as YAML multiline list (Claude native format)
439
451
  const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
440
452
  let toolsBlock = '';
@@ -474,19 +486,100 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
474
486
  fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
475
487
  if (agent)
476
488
  fm += `agent: ${agent}\n`;
477
- // #769: emit context: and effort: when present so the runtime can honour
478
- // them natively (context: fork = isolated subagent window; effort: =
479
- // token-budget tier). Fields are Claude-specific; unknown frontmatter
480
- // 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.)
481
495
  if (context)
482
496
  fm += `context: ${context}\n`;
483
- if (effort)
484
- fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
485
497
  if (toolsBlock)
486
498
  fm += toolsBlock;
487
499
  fm += '---';
488
500
  return `${fm}\n${normalizedBody}`;
489
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
+ }
490
583
  function normalizeKimiSkillName(skillName) {
491
584
  let text = String(skillName || '').trim().toLowerCase();
492
585
  if (text.startsWith('/'))
@@ -502,7 +595,7 @@ function normalizeKimiSkillName(skillName) {
502
595
  function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
503
596
  if (!Array.isArray(cmdNames) || cmdNames.length === 0)
504
597
  return content;
505
- 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);
506
599
  const commandGroup = commands.join('|');
507
600
  const colonPattern = new RegExp(`(?<![A-Za-z0-9_/:.-])/?gsd:(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
508
601
  const hyphenPattern = new RegExp(`(?:/|\\$)gsd-(${commandGroup})(?=[^A-Za-z0-9_-]|$)`, 'g');
@@ -1533,7 +1626,7 @@ Typed mapping (agent_type-capable schema only):
1533
1626
  to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
1534
1627
  inherited, or unsupported values; do not invent one-off effort literals in
1535
1628
  workflow prose.
1536
- - \`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
1537
1630
  - \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
1538
1631
  - \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
1539
1632
  - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
@@ -2188,6 +2281,106 @@ function convertClaudeAgentToQwenAgent(content) {
2188
2281
  fm += '---';
2189
2282
  return `${fm}\n${body}`;
2190
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
+ }
2191
2384
  function convertClaudeAgentToCodebuddyAgent(content) {
2192
2385
  const converted = convertClaudeToCodebuddyMarkdown(content);
2193
2386
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2208,6 +2401,48 @@ function convertClaudeAgentToClineAgent(content) {
2208
2401
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
2209
2402
  return `${cleanFrontmatter}\n${body}`;
2210
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
+ }
2211
2446
  /**
2212
2447
  * Convert Claude Code agent markdown to Codex agent format.
2213
2448
  * Applies base markdown conversions, then adds a <codex_agent_role> header
@@ -2320,20 +2555,145 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
2320
2555
  const NON_CLAUDE_RUNTIMES = Object.keys(capabilityRegistry.runtimes)
2321
2556
  .filter((id) => id !== 'claude')
2322
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
+ }
2323
2595
  /**
2324
2596
  * #1521: Every non-Claude runtime resolves its own runtime identity from a
2325
- * runtime-neutral config, and defaults workflow.use_worktrees to false —
2326
- * GSD's worktree isolation uses Claude Code's isolation="worktree" spawn
2327
- * parameter, which no other runtime honors. Stamped into the emitted
2328
- * 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.
2329
2612
  *
2330
2613
  * @private — exported as `_stampNonClaudeRuntimeDefaults` for tests.
2331
2614
  */
2332
2615
  function _stampNonClaudeRuntimeDefaults(content, runtime) {
2333
- 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
+ }
2334
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}"`);
2335
2620
  return content;
2336
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
+ }
2337
2697
  /**
2338
2698
  * Apply the per-runtime rewrite table to a single content string.
2339
2699
  * Relocated from bin/install.js `_applyRuntimeRewrites`.
@@ -2410,7 +2770,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2410
2770
  // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
2411
2771
  // dirName-derived (no runtime literal). getDirName('augment') resolves
2412
2772
  // to '.augment', so this is byte-identical to the prior hardcoded regexes.
2413
- const _dd = escapeRegExp(dirName);
2773
+ const _dd = (0, pattern_cjs_1.escapeRegex)(dirName);
2414
2774
  content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
2415
2775
  content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
2416
2776
  content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
@@ -2428,7 +2788,7 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2428
2788
  // #2094: descriptor-driven — dirName resolves to '.trae' via
2429
2789
  // getDirName()/localConfigDir, so this regex is built rather than
2430
2790
  // hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
2431
- content = content.replace(new RegExp('~/' + escapeRegExp(dirName) + '/', 'g'), pathPrefix);
2791
+ content = content.replace(new RegExp('~/' + (0, pattern_cjs_1.escapeRegex)(dirName) + '/', 'g'), pathPrefix);
2432
2792
  content = processAttribution(content, attribution);
2433
2793
  break;
2434
2794
  case 'codebuddy':
@@ -2454,6 +2814,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2454
2814
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2455
2815
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2456
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);
2457
2821
  content = processAttribution(content, attribution);
2458
2822
  break;
2459
2823
  // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
@@ -2541,18 +2905,18 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2541
2905
  * @param attribution Co-Authored-By value (string | null | undefined)
2542
2906
  */
2543
2907
  function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2544
- if (!node_fs_1.default.existsSync(stagedDir))
2908
+ if (!installFs().existsSync(stagedDir))
2545
2909
  return;
2546
2910
  const walkAndRewrite = (dir) => {
2547
- for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
2911
+ for (const entry of installFs().readdirSync(dir, { withFileTypes: true })) {
2548
2912
  const fullPath = node_path_1.default.join(dir, entry.name);
2549
2913
  if (entry.isDirectory()) {
2550
2914
  walkAndRewrite(fullPath);
2551
2915
  }
2552
2916
  else if (entry.name.endsWith('.md')) {
2553
- let content = node_fs_1.default.readFileSync(fullPath, 'utf8');
2917
+ let content = installFs().readFileSync(fullPath, 'utf8');
2554
2918
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2555
- node_fs_1.default.writeFileSync(fullPath, content);
2919
+ installFs().writeFileSync(fullPath, content);
2556
2920
  }
2557
2921
  }
2558
2922
  };
@@ -2577,14 +2941,14 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
2577
2941
  * @returns {string} path to the temp dir (caller is responsible for cleanup)
2578
2942
  */
2579
2943
  function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
2580
- if (!node_fs_1.default.existsSync(stagedDir))
2944
+ if (!installFs().existsSync(stagedDir))
2581
2945
  return stagedDir;
2582
- 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-');
2583
2947
  try {
2584
- for (const entry of node_fs_1.default.readdirSync(stagedDir, { withFileTypes: true })) {
2948
+ for (const entry of installFs().readdirSync(stagedDir, { withFileTypes: true })) {
2585
2949
  if (!entry.isFile() || !entry.name.endsWith('.md'))
2586
2950
  continue;
2587
- 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');
2588
2952
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2589
2953
  // #2097 (ADR-1239): descriptor-driven — commandBodyConverter name comes
2590
2954
  // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
@@ -2592,18 +2956,52 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2592
2956
  if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
2593
2957
  content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
2594
2958
  }
2595
- 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);
2596
2960
  }
2597
2961
  }
2598
2962
  catch (err) {
2599
2963
  try {
2600
- node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
2964
+ installFs().rmSync(tempDir, { recursive: true, force: true });
2601
2965
  }
2602
2966
  catch { /* best-effort */ }
2603
2967
  throw err;
2604
2968
  }
2605
2969
  return tempDir;
2606
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
+ }
2607
3005
  /**
2608
3006
  * HIGH-LEVEL: In-place fs walk: rewrite all .md files under stagedDir for the given runtime.
2609
3007
  *
@@ -2620,16 +3018,30 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2620
3018
  */
2621
3019
  function rewriteStagedSkillBodies(stagedDir, opts) {
2622
3020
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2623
- if (!node_fs_1.default.existsSync(stagedDir))
3021
+ if (!installFs().existsSync(stagedDir))
2624
3022
  return;
2625
3023
  const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2626
3024
  const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2627
- 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);
2628
3029
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2629
3030
  const isWindowsHost = platform === 'win32';
2630
3031
  const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
2631
3032
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2632
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
+ }
2633
3045
  }
2634
3046
  /**
2635
3047
  * HIGH-LEVEL: Copy-to-temp then rewrite all .md files for the given runtime.
@@ -2649,11 +3061,14 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
2649
3061
  */
2650
3062
  function rewriteStagedCommandBodies(stagedDir, opts) {
2651
3063
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2652
- if (!node_fs_1.default.existsSync(stagedDir))
3064
+ if (!installFs().existsSync(stagedDir))
2653
3065
  return stagedDir;
2654
3066
  const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2655
3067
  const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2656
- 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);
2657
3072
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2658
3073
  const isWindowsHost = platform === 'win32';
2659
3074
  const pathPrefix = computePathPrefix({ isGlobal, isOpencode, isWindowsHost, resolvedTarget, homeDir });
@@ -2714,6 +3129,127 @@ function applyAgentPathRewrites(content, runtime, pathPrefix) {
2714
3129
  return content;
2715
3130
  }
2716
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
+ }
2717
3253
  /**
2718
3254
  * Apply Co-Authored-By attribution policy to file content.
2719
3255
  * - null -> remove the Co-Authored-By line and its preceding blank line
@@ -2756,6 +3292,11 @@ module.exports = {
2756
3292
  convertClaudeToAntigravityContent,
2757
3293
  convertClaudeCommandToAntigravitySkill,
2758
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,
2759
3300
  convertClaudeCommandToKimiSkill,
2760
3301
  convertClaudeCommandToKimiCodeSkill,
2761
3302
  buildKimiAgentArtifacts,
@@ -2810,11 +3351,20 @@ module.exports = {
2810
3351
  convertClaudeAgentToCodebuddyAgent,
2811
3352
  convertClaudeAgentToClineAgent,
2812
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,
2813
3358
  // ADR-1239 / #2092 Phase B Upgrade 1: native .qwen/agents/*.md subagent
2814
3359
  // projection — registered by name so convertedAgentsKind's
2815
3360
  // conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
2816
3361
  // can resolve it from capabilities/qwen/capability.json's agents kind.
2817
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,
2818
3368
  // #1511 ADR-1508 Phase 2: rewrite engine deep seam
2819
3369
  // Low-level walkers (pathPrefix + attribution pre-resolved by caller):
2820
3370
  applyRuntimeContentRewritesInPlace,
@@ -2825,9 +3375,21 @@ module.exports = {
2825
3375
  // ADR-1235 §1: descriptor-driven agent cross-cutting
2826
3376
  applyAgentPathRewrites,
2827
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,
2828
3387
  _computePathPrefix: computePathPrefix,
3388
+ _restoreClaudeGlobalAtRefTilde: restoreClaudeGlobalAtRefTilde,
2829
3389
  _applyRuntimeRewrites,
2830
3390
  _stampNonClaudeRuntimeDefaults,
3391
+ // #2652: registry-resolved dispatch isolation, mirroring routeDispatchIsolation
3392
+ _negotiatedDispatchIsolation,
2831
3393
  // #1521: canonical non-Claude runtime list for test files and tooling
2832
3394
  NON_CLAUDE_RUNTIMES,
2833
3395
  };