@opengsd/gsd-core 1.12.0 → 1.14.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 (455) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -23,6 +23,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
23
23
  const node_os_1 = __importDefault(require("node:os"));
24
24
  const node_path_1 = __importDefault(require("node:path"));
25
25
  const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
26
+ const security_cjs_1 = require("./security.cjs");
26
27
  const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
27
28
  const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
28
29
  const runtimeNamePolicy = require("./runtime-name-policy.cjs");
@@ -55,6 +56,148 @@ const { processAttribution } = runtimeArtifactConversion;
55
56
  // resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
56
57
  // test stubs that monkeypatch the module's exports are seen at call time.
57
58
  const { getDirName } = runtimeNamePolicy;
59
+ function withInstallerPackageSource(configDir, fn) {
60
+ // Reuse the existing compatibility-marker contract through the existing fs
61
+ // seam. The marker exists only in this synchronous call tree: no disk state,
62
+ // layout export, stage argument, or caller-settable authority flag is added.
63
+ const markerPath = node_path_1.default.resolve(configDir, '.gsd-source');
64
+ const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
65
+ const markerBytes = Buffer.from(packageCommandsRoot + '\n');
66
+ const base = installFs();
67
+ const overlay = {
68
+ ...base,
69
+ existsSync: (candidate) => node_path_1.default.resolve(candidate) === markerPath || base.existsSync(candidate),
70
+ lstatSync: (candidate) => node_path_1.default.resolve(candidate) === markerPath
71
+ ? { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
72
+ : base.lstatSync(candidate),
73
+ readFileSync: ((candidate, encoding) => {
74
+ if (node_path_1.default.resolve(candidate) !== markerPath) {
75
+ return encoding ? base.readFileSync(candidate, encoding) : base.readFileSync(candidate);
76
+ }
77
+ return encoding ? markerBytes.toString(encoding) : Buffer.from(markerBytes);
78
+ }),
79
+ };
80
+ return withInstallFs(overlay, fn);
81
+ }
82
+ function isRuntimeSurfaceSourceUnavailable(message) {
83
+ return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
84
+ message.endsWith('install or upgrade gsd-core before materializing this surface.');
85
+ }
86
+ function assertCorpusTreeHasNoSymlinks(root) {
87
+ if (!installFs().existsSync(root))
88
+ return;
89
+ const stat = installFs().lstatSync(root);
90
+ if (stat.isSymbolicLink()) {
91
+ throw new Error(`Runtime Surface corpus path is a symlink: ${root}`);
92
+ }
93
+ if (!stat.isDirectory())
94
+ return;
95
+ for (const name of installFs().readdirSync(root)) {
96
+ assertCorpusTreeHasNoSymlinks(node_path_1.default.join(root, name));
97
+ }
98
+ }
99
+ function previousOwnedCorpusFiles(configDir, prefix) {
100
+ try {
101
+ const files = installerMigrations.readInstallManifest(configDir).files;
102
+ return Object.keys(files)
103
+ .filter((entry) => entry.startsWith(prefix))
104
+ .map((entry) => entry.slice(prefix.length))
105
+ .filter((entry) => entry !== '' && !node_path_1.default.posix.isAbsolute(entry) && !entry.split('/').some((part) => part === '' || part === '.' || part === '..'));
106
+ }
107
+ catch {
108
+ // An absent or unreadable prior manifest provides no ownership evidence.
109
+ // Preserve existing entries rather than guessing that they are stale.
110
+ return [];
111
+ }
112
+ }
113
+ function pruneEmptyCorpusParents(start, stop) {
114
+ let current = node_path_1.default.dirname(start);
115
+ while (current !== stop && current.startsWith(stop + node_path_1.default.sep)) { // allow-handrolled-containment: ancestor-walk loop condition, not a containment gate
116
+ if (installFs().readdirSync(current).length > 0)
117
+ return;
118
+ installFs().rmdirSync(current);
119
+ current = node_path_1.default.dirname(current);
120
+ }
121
+ }
122
+ function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix) {
123
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
124
+ throw new Error(`syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${configDir}" does not trust — refusing to write.`);
125
+ }
126
+ assertCorpusTreeHasNoSymlinks(destination);
127
+ // Remove only paths the previous manifest proves GSD owned and which the
128
+ // executing package no longer ships. Unknown neighbouring files survive.
129
+ for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
130
+ const sourceEntry = node_path_1.default.join(source, ...relative.split('/'));
131
+ let sourceIsFile = false;
132
+ try {
133
+ sourceIsFile = installFs().lstatSync(sourceEntry).isFile();
134
+ }
135
+ catch {
136
+ sourceIsFile = false;
137
+ }
138
+ if (sourceIsFile)
139
+ continue;
140
+ const target = node_path_1.default.join(destination, ...relative.split('/'));
141
+ if (!installFs().existsSync(target))
142
+ continue;
143
+ const targetStat = installFs().lstatSync(target);
144
+ if (!targetStat.isFile()) {
145
+ throw new Error(`Runtime Surface corpus ownership conflict at ${target}`);
146
+ }
147
+ installFs().rmSync(target, { force: true });
148
+ pruneEmptyCorpusParents(target, destination);
149
+ }
150
+ installFs().mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
151
+ installFs().cpSync(source, destination, { recursive: true });
152
+ }
153
+ /**
154
+ * Provision the raw, installation-owned input needed to re-materialize a
155
+ * global Runtime Surface after the executing package tree disappears.
156
+ *
157
+ * The corpus deliberately lives below the already-installed `gsd-core/`
158
+ * tree and is accepted only after its manifest ownership and hashes verify.
159
+ * The compatibility marker does not replace that installed-corpus authority.
160
+ */
161
+ function provisionRuntimeSurfaceCorpus(layout, configDir, scope) {
162
+ const required = new Set();
163
+ if ((0, install_scope_cjs_1.isGlobalScope)(scope)) {
164
+ for (const kind of layout.kinds) {
165
+ if (kind.kind === 'commands' || kind.kind === 'skills')
166
+ required.add('commands');
167
+ if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
168
+ required.add('agents');
169
+ }
170
+ }
171
+ if (required.size === 0)
172
+ return;
173
+ const corpusRoot = node_path_1.default.join(configDir, 'gsd-core');
174
+ if (required.has('commands')) {
175
+ const source = runtimeArtifactLayout.findInstallSourceRoot();
176
+ const destination = node_path_1.default.join(corpusRoot, 'commands', 'gsd');
177
+ syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
178
+ }
179
+ if (required.has('agents')) {
180
+ const source = node_path_1.default.join(executingPackageRoot(), 'agents');
181
+ const destination = node_path_1.default.join(corpusRoot, 'agents');
182
+ syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
183
+ }
184
+ const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
185
+ if (typeof markerFile === 'string' && markerFile !== '' && required.has('commands')) {
186
+ try {
187
+ const markerPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, markerFile);
188
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
189
+ throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
190
+ }
191
+ installFs().writeFileSync(markerPath, node_path_1.default.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
192
+ }
193
+ catch {
194
+ // The existing installer marker writer owns the user-facing warning and
195
+ // keeps marker failure non-fatal. The installed corpus remains usable
196
+ // without the compatibility marker.
197
+ }
198
+ }
199
+ }
200
+ function executingPackageRoot() { return node_path_1.default.dirname(node_path_1.default.dirname(runtimeArtifactLayout.findInstallSourceRoot())); }
58
201
  // ---------------------------------------------------------------------------
59
202
  // USER_OWNED_ARTIFACTS
60
203
  // ---------------------------------------------------------------------------
@@ -205,8 +348,11 @@ function hasExistingSymlinkBetween(root, fullPath, options = {}) {
205
348
  const resolvedFullPath = node_path_1.default.resolve(fullPath);
206
349
  // (a) Path-traversal refusal — ALWAYS enforced, even with opt-in. An untrusted
207
350
  // destSubpath string that escapes the install root via '..' is rejected
208
- // regardless of user opt-in state (ADR-1239 Phase B threat (a)).
209
- if (resolvedFullPath !== resolvedRoot && !resolvedFullPath.startsWith(resolvedRoot + node_path_1.default.sep)) {
351
+ // regardless of user opt-in state (ADR-1239 Phase B threat (a)). Lexical
352
+ // (ADR-4650 decision 6): this function's whole purpose is to DETECT
353
+ // symlinks between root and target, so resolving them here would erase what
354
+ // it measures.
355
+ if ((0, security_cjs_1.tryWithinRootLexical)(resolvedFullPath, resolvedRoot) === null) {
210
356
  return true;
211
357
  }
212
358
  // #2393 (security-review finding): realpathSync fully resolves all symlink
@@ -977,6 +1123,7 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
977
1123
  */
978
1124
  function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, capabilityRegistry, deps = {}) {
979
1125
  return withInstallFs(deps.fs, () => {
1126
+ const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope, capabilityRegistry);
980
1127
  // A removed descriptor kind is no longer visited by the layout loop, so it
981
1128
  // cannot prune its own previous output. Clean manifest-proven retired files
982
1129
  // before materializing the current layout (#2644).
@@ -986,6 +1133,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
986
1133
  // generic layout-driven loop below, mirroring the bespoke install path that
987
1134
  // previously lived inline in bin/install.js.
988
1135
  const behaviors = _hostBehaviors(runtime);
1136
+ const projectDir = scope === 'global' ? process.cwd() : configDir;
989
1137
  if (behaviors.combinedFamilyInstall) {
990
1138
  // #2329: combined-family runtimes (OpenCode/Kilo) bypass
991
1139
  // _runLegacyInstallMigrations below entirely (early return), so their
@@ -994,17 +1142,24 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
994
1142
  // #2874 design row 2: this early return must ALSO return an executed
995
1143
  // plan — installOpencodeFamilyArtifacts reports what it wrote, so a
996
1144
  // whole runtime family returning undefined is no longer a hole.
997
- return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry);
1145
+ // An injected filesystem supplies its own hermetic corpus fixture. The
1146
+ // real installer is the authority that refreshes package bytes into the
1147
+ // durable installed corpus; attempting that cross-filesystem copy through
1148
+ // an in-memory destination adapter would read from the wrong filesystem.
1149
+ if (!deps.fs)
1150
+ provisionRuntimeSurfaceCorpus(layout, configDir, scope);
1151
+ return installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry, deps.packageRoot, projectDir);
998
1152
  }
999
1153
  // Legacy cleanup before layout-driven writes
1000
1154
  _runLegacyInstallMigrations(runtime, configDir, scope);
1001
- const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope, capabilityRegistry);
1155
+ if (!deps.fs)
1156
+ provisionRuntimeSurfaceCorpus(layout, configDir, scope);
1002
1157
  // #3712: a global `home` override escapes the sandboxed configDir. Refuse to
1003
1158
  // execute when a test run would land that escape in the developer's real home.
1004
1159
  testHomeGuard.assertTestHomeSandboxed('installRuntimeArtifacts', runtime, layout?.kinds, {
1005
1160
  os: deps.os, env: deps.env,
1006
1161
  });
1007
- const planResult = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
1162
+ const createPlan = () => runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
1008
1163
  // `Layout` is structurally identical across the layout/install-plan .cjs
1009
1164
  // modules but nominally distinct to tsc (untyped .cjs boundary) — bridge it.
1010
1165
  layout: layout,
@@ -1012,7 +1167,16 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
1012
1167
  homedir: () => node_os_1.default.homedir(),
1013
1168
  platform: process.platform,
1014
1169
  resolveAttribution,
1170
+ projectDir,
1015
1171
  });
1172
+ let planResult = createPlan();
1173
+ if (scope === 'global' &&
1174
+ !planResult.ok &&
1175
+ planResult.kind === 'stage_failed' &&
1176
+ planResult.cleanupDirs.length === 0 &&
1177
+ isRuntimeSurfaceSourceUnavailable(planResult.message)) {
1178
+ planResult = withInstallerPackageSource(configDir, createPlan);
1179
+ }
1016
1180
  const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
1017
1181
  // #2874 row 1/4/5: per-kind executed-plan entries, appended only as the
1018
1182
  // loop below actually finishes writing each kind — a kind that throws
@@ -1176,8 +1340,10 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
1176
1340
  // is safe even when configDir has no .gsd-source marker (artifactLayout: []).
1177
1341
  let nativePluginInstalled = false;
1178
1342
  if (behaviors.nativePlugin) {
1179
- const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
1180
- const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
1343
+ // Native plugin sources live only in the executing package, never in the
1344
+ // durable Runtime Surface corpus. Do not derive this package root from a
1345
+ // config-scoped provider that may now correctly resolve installed input.
1346
+ const src = deps.packageRoot ?? executingPackageRoot();
1181
1347
  _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
1182
1348
  nativePluginInstalled = true;
1183
1349
  }
@@ -1330,6 +1496,15 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
1330
1496
  content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
1331
1497
  content = processAttribution(content, resolveAttribution(runtime));
1332
1498
  const skillDir = node_path_1.default.join(dest, skillName);
1499
+ // isPathConfined is lexical and cannot see a symlink. mkdirSync({recursive:true})
1500
+ // does NOT throw when skillDir already exists as a symlink to a directory, so a
1501
+ // pre-planted link would redirect the SKILL.md write outside `dest`. Refuse to
1502
+ // write through a link (epic #4636; mirrors retired-artifact-cleanup.cts:77).
1503
+ try {
1504
+ if (installFs().lstatSync(skillDir).isSymbolicLink())
1505
+ continue;
1506
+ }
1507
+ catch { /* ENOENT: not created yet — the normal case */ }
1333
1508
  installFs().mkdirSync(skillDir, { recursive: true });
1334
1509
  installFs().writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
1335
1510
  // #2322 HIGH-3 parity: persist the capability-owned marker so a later
@@ -1388,12 +1563,13 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
1388
1563
  * @param capabilityRegistry - #2362: optional composed capability registry, threaded
1389
1564
  * straight through to resolveRuntimeArtifactLayout (unused by the agents kind today,
1390
1565
  * but kept for signature parity with the skills/commands siblings on this call tree)
1566
+ * @param projectDir - project/config discovery root, distinct from the artifact destination
1391
1567
  * @returns `{ sourceDir, destDir }` describing what was written, or `null` when the
1392
1568
  * runtime's layout declares no `agents` kind.
1393
1569
  */
1394
- function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile, pathPrefix, resolveAttribution = () => undefined, capabilityRegistry) {
1570
+ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile, pathPrefix, resolveAttribution = () => undefined, capabilityRegistry, projectDir) {
1395
1571
  const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope, capabilityRegistry);
1396
- const agentsKindEntry = layout.kinds.find((k) => k.kind === 'agents');
1572
+ const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
1397
1573
  if (!agentsKindEntry)
1398
1574
  return null;
1399
1575
  // #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
@@ -1408,8 +1584,16 @@ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile,
1408
1584
  // for the generic layout-driven loop (runtime-artifact-install-plan.cts) —
1409
1585
  // targetDir IS the install root the inline agent loop called `targetDir`.
1410
1586
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
1411
- const agentCtx = { runtime, pathPrefix, attribution, targetDir };
1412
- const stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
1587
+ const agentCtx = { runtime, pathPrefix, attribution, targetDir, projectDir: projectDir ?? targetDir };
1588
+ let stagedDir;
1589
+ try {
1590
+ stagedDir = agentsKindEntry.stage(resolvedProfile, agentCtx);
1591
+ }
1592
+ catch (err) {
1593
+ if (scope !== 'global' || !isRuntimeSurfaceSourceUnavailable(err.message))
1594
+ throw err;
1595
+ stagedDir = withInstallerPackageSource(targetDir, () => agentsKindEntry.stage(resolvedProfile, agentCtx));
1596
+ }
1413
1597
  const stagedAgentFiles = installFs().existsSync(stagedDir)
1414
1598
  ? installFs().readdirSync(stagedDir).filter((f) => f.endsWith('.md'))
1415
1599
  : [];
@@ -1670,12 +1854,13 @@ function _migrateLegacyOpencodeCommandDir(runtime, configDir, behaviors) {
1670
1854
  * installOpencodeFamilySkills so an installed third-party capability skill
1671
1855
  * materializes for this combined-family (OpenCode/Kilo) install path too.
1672
1856
  * Absent -> no third-party skills staged (fail closed).
1857
+ * @param projectDir - project/config discovery root, distinct from configDir for global installs
1673
1858
  * @returns #2874 design row 2: an executed-plan value, same top-level shape
1674
1859
  * (`runtime`/`scope`/`kinds`/`cleanup`/`postSteps`) as the generic
1675
1860
  * `installRuntimeArtifacts` branch — this was the one early return a
1676
1861
  * `void`-shaped hole survived unnoticed in.
1677
1862
  */
1678
- function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}, capabilityRegistry) {
1863
+ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}, capabilityRegistry, packageRoot, projectDir) {
1679
1864
  // #2870: `scope` keeps its exported required `string` signature (no
1680
1865
  // signature change). It is always the `installRuntimeArtifacts`-forwarded
1681
1866
  // 'global' | 'local' literal produced by bin/install.js's scope-resolution
@@ -1688,7 +1873,7 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1688
1873
  // into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
1689
1874
  // root (needed below for the native plugin source) is two levels up.
1690
1875
  const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
1691
- const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
1876
+ const src = packageRoot ?? executingPackageRoot();
1692
1877
  const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
1693
1878
  const pathPrefix = runtimeArtifactConversion._computePathPrefix({
1694
1879
  isGlobal,
@@ -1711,7 +1896,7 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1711
1896
  // generic layout-driven loop uses (see installAgentsKindStandalone's own
1712
1897
  // doc). A `null` result means this runtime's layout declares no `agents`
1713
1898
  // kind — nothing written, nothing reported (no #1879-F15 inert claim).
1714
- const agentsResult = installAgentsKindStandalone(runtime, configDir, scope, resolvedProfile, pathPrefix, resolveAttribution, capabilityRegistry);
1899
+ const agentsResult = installAgentsKindStandalone(runtime, configDir, scope, resolvedProfile, pathPrefix, resolveAttribution, capabilityRegistry, projectDir);
1715
1900
  _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
1716
1901
  // #2874 design row 2: report what this combined-family install wrote,
1717
1902
  // mirroring the generic branch's top-level shape. `cleanup` is `[]` — this
@@ -114,6 +114,50 @@ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
114
114
  // Per-project wins on conflict; preserve non-conflicting global keys.
115
115
  return { ...(global || {}), ...(projectOverrides || {}) };
116
116
  }
117
+ function readGsdAgentTools(config) {
118
+ const raw = config?.agent_tools;
119
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
120
+ return null;
121
+ const result = {};
122
+ for (const [selector, value] of Object.entries(raw)) {
123
+ // An explicitly present selector always has a verdict. Invalid values are
124
+ // empty so a project config cannot accidentally restore a global grant.
125
+ result[selector] = Array.isArray(value)
126
+ ? value.filter((entry) => typeof entry === 'string')
127
+ .map((entry) => entry.trim())
128
+ .filter((entry) => entry.length > 0
129
+ && !entry.endsWith(':')
130
+ && !/[\s\u0000-\u001F\u007F-\u009F,#"'\u2028\u2029]/.test(entry))
131
+ : [];
132
+ }
133
+ return result;
134
+ }
135
+ /**
136
+ * Resolve valid install-time `agent_tools` grants from global defaults and
137
+ * the nearest project config. Project selectors replace only matching global
138
+ * selectors; malformed whole files remain harmless absence like the existing
139
+ * model override resolver.
140
+ */
141
+ function readGsdEffectiveAgentTools(targetDir = null, options = {}) {
142
+ const home = options.homedir ? options.homedir() : node_os_1.default.homedir();
143
+ const globalConfig = _readGsdConfigFile(node_path_1.default.join(home, '.gsd', 'defaults.json'), 'global defaults');
144
+ let projectConfig = null;
145
+ if (targetDir) {
146
+ const candidate = _findAncestorGsdConfigPath(targetDir);
147
+ if (candidate) {
148
+ projectConfig = _readGsdConfigFile(candidate, 'project config');
149
+ }
150
+ }
151
+ const global = readGsdAgentTools(globalConfig);
152
+ const project = readGsdAgentTools(projectConfig);
153
+ if (projectConfig
154
+ && Object.prototype.hasOwnProperty.call(projectConfig, 'agent_tools')
155
+ && project === null)
156
+ return {};
157
+ if (!global && !project)
158
+ return null;
159
+ return { ...(global || {}), ...(project || {}) };
160
+ }
117
161
  /**
118
162
  * Build a runtime-aware tier resolver for the install path (#2517).
119
163
  *
@@ -230,6 +274,7 @@ function resolveAgentModelOverride(agentName, modelOverrides, runtimeResolver) {
230
274
  module.exports = {
231
275
  readGsdGlobalModelOverrides,
232
276
  readGsdEffectiveModelOverrides,
277
+ readGsdEffectiveAgentTools,
233
278
  readGsdRuntimeProfileResolver,
234
279
  resolveAgentModelOverride,
235
280
  };
@@ -32,7 +32,7 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
32
32
  const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const conversionModule = require("./runtime-artifact-conversion.cjs");
35
- const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, } = conversionModule;
35
+ const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, } = conversionModule;
36
36
  // #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
37
37
  // stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
38
38
  // `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
@@ -40,6 +40,8 @@ const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _pr
40
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
41
41
  const workflowFragmentsModule = require("./workflow-fragments.cjs");
42
42
  const { composeWorkflow: _composeWorkflow } = workflowFragmentsModule;
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const installModelOverrideResolver = require("./install-model-override-resolver.cjs");
43
45
  // ---------------------------------------------------------------------------
44
46
  // Profile definitions
45
47
  // ---------------------------------------------------------------------------
@@ -738,6 +740,11 @@ function readInstalledCapabilitySkill(stem, registry) {
738
740
  return null;
739
741
  const skillPath = node_path_1.default.join(capDir, relSkillPath);
740
742
  try {
743
+ // statSync follows symlinks; isPathConfined is lexical and cannot see one.
744
+ // Refuse to read through a link so an outside file's content cannot be
745
+ // installed as a capability skill (epic #4636).
746
+ if (node_fs_1.default.lstatSync(skillPath).isSymbolicLink())
747
+ return null;
741
748
  if (!node_fs_1.default.statSync(skillPath).isFile())
742
749
  return null;
743
750
  return { capId, content: node_fs_1.default.readFileSync(skillPath, 'utf8') };
@@ -856,6 +863,15 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
856
863
  if (!(0, external_descriptor_trust_cjs_1.isPathConfined)(skillName, stageDir))
857
864
  continue; // defense-in-depth
858
865
  const destDir = node_path_1.default.join(stageDir, skillName);
866
+ // isPathConfined is lexical and cannot see a symlink. mkdirSync({recursive:true})
867
+ // does NOT throw when destDir already exists as a symlink to a directory, so a
868
+ // pre-planted link would redirect the SKILL.md write outside `stageDir`. Refuse
869
+ // to write through a link (epic #4636; mirrors retired-artifact-cleanup.cts:77).
870
+ try {
871
+ if (installFs().lstatSync(destDir).isSymbolicLink())
872
+ continue;
873
+ }
874
+ catch { /* ENOENT: not created yet — the normal case */ }
859
875
  installFs().mkdirSync(destDir, { recursive: true });
860
876
  installFs().writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), found.content);
861
877
  // #2322 HIGH-3: persist the capability-owned marker so a later prune
@@ -896,14 +912,14 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
896
912
  * agent loop in bin/install.js exactly:
897
913
  * 1. applyAgentPathRewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
898
914
  * 2. processAttribution (Co-Authored-By policy)
899
- * 3. converter (runtime-specific frontmatter/body transform)
900
- * 4. applyAgentFrontmatterExtensions (#2875 Part 2: effort/disallowedTools,
915
+ * 3. appendAgentTools (#4032: validated agent_tools grants, before host conversion)
916
+ * 4. converter (runtime-specific frontmatter/body transform)
917
+ * 5. applyAgentFrontmatterExtensions (#2875 Part 2: effort/disallowedTools,
901
918
  * gated by hostBehaviors.agentFrontmatterExtensions — no-op for a runtime
902
919
  * that declares nothing, e.g. every non-Claude runtime today)
903
- * 5. normalizeAgentBodyForRuntime (colon→hyphen refs; no-op for trivial group)
904
- * When `agentCtx` is absent, only the converter is applied (backward-compat for
905
- * the feat-1173 synthetic-descriptor tests and the copilot/antigravity paths
906
- * that handle cross-cutting inside their converters).
920
+ * 6. normalizeAgentBodyForRuntime (colon→hyphen refs; no-op for trivial group)
921
+ * When `agentCtx` is absent, only global `agent_tools` augmentation and the
922
+ * converter run; other cross-cutting remains absent for backward compatibility.
907
923
  *
908
924
  * @param srcAgentsDir source agents directory (e.g. agents/)
909
925
  * @param resolvedProfile profile filter from resolveProfile()
@@ -917,8 +933,7 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
917
933
  * every OTHER converter's contract — converters that don't declare a
918
934
  * 3rd parameter simply never read it.
919
935
  * @param isGlobal install scope passed through to the converter
920
- * @param agentCtx optional cross-cutting context (ADR-1235 §1); when absent,
921
- * only the converter is applied (backward compat)
936
+ * @param agentCtx optional cross-cutting context (ADR-1235 §1)
922
937
  */
923
938
  function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false, agentCtx) {
924
939
  if (!installFs().existsSync(srcAgentsDir))
@@ -950,6 +965,7 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
950
965
  try {
951
966
  // Resolve cmdNames once per staging call (not per file) for performance.
952
967
  const cmdNames = agentCtx ? _readGsdCommandNames() : [];
968
+ const agentTools = installModelOverrideResolver.readGsdEffectiveAgentTools(agentCtx?.projectDir ?? agentCtx?.targetDir ?? null);
953
969
  for (const entry of entries) {
954
970
  if (!entry.isFile())
955
971
  continue;
@@ -969,25 +985,31 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
969
985
  // throws loudly naming the file for a malformed marker, never emitting a
970
986
  // half-composed agent.
971
987
  content = _composeWorkflow(content, { sourcePath: agentSourcePath });
988
+ const agentName = _deriveAgentName(entry.name);
989
+ const grants = [
990
+ ...(agentTools?.['*'] || []),
991
+ ...(agentTools?.[agentName] || []),
992
+ ];
972
993
  if (agentCtx) {
973
994
  // #2875 Part 2 / row I3: derived exactly as the inline loop does —
974
995
  // single-sourced via deriveAgentName (runtime-artifact-conversion.cts).
975
- const agentName = _deriveAgentName(entry.name);
976
996
  // ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly)
977
997
  // Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
978
998
  content = _applyAgentPathRewrites(content, agentCtx.runtime, agentCtx.pathPrefix);
979
999
  // Step 2: attribution
980
1000
  content = _processAttribution(content, agentCtx.attribution);
981
- // Step 3: converter (runtime-specific frontmatter/body transform)
1001
+ // Step 3: validated canonical grants, before host conversion.
1002
+ content = _appendAgentTools(content, grants);
1003
+ // Step 4: converter (runtime-specific frontmatter/body transform)
982
1004
  content = converter(content, isGlobal, { agentName });
983
- // Step 4: frontmatter extensions (effort/disallowedTools; no-op unless
1005
+ // Step 5: frontmatter extensions (effort/disallowedTools; no-op unless
984
1006
  // the runtime declares hostBehaviors.agentFrontmatterExtensions)
985
1007
  content = _applyAgentFrontmatterExtensions(content, { runtime: agentCtx.runtime, agentName, targetDir: agentCtx.targetDir });
986
- // Step 5: normalize colon→hyphen refs (no-op for trivial group)
1008
+ // Step 6: normalize colon→hyphen refs (no-op for trivial group)
987
1009
  content = _normalizeAgentBodyForRuntime(content, agentCtx.runtime, cmdNames);
988
1010
  }
989
1011
  else {
990
- // Backward-compat: only apply the converter (no cross-cutting)
1012
+ content = _appendAgentTools(content, grants);
991
1013
  content = converter(content, isGlobal);
992
1014
  }
993
1015
  installFs().writeFileSync(node_path_1.default.join(stageDir, entry.name), content, 'utf8');
@@ -55,6 +55,7 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
55
55
  'hooks/gsd-prompt-guard.js',
56
56
  'hooks/gsd-read-guard.js',
57
57
  'hooks/gsd-read-injection-scanner.js',
58
+ 'hooks/gsd-secret-read-guard.js',
58
59
  'hooks/gsd-session-state.sh',
59
60
  'hooks/gsd-statusline.js',
60
61
  'hooks/gsd-update-banner.js',
@@ -17,6 +17,7 @@ const installer_migration_authoring_cjs_1 = require("./installer-migration-autho
17
17
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
18
  const clock_cjs_1 = require("./clock.cjs");
19
19
  const install_scope_cjs_1 = require("./install-scope.cjs");
20
+ const security_cjs_1 = require("./security.cjs");
20
21
  // #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
21
22
  // plan/apply/rollback/lock/journal engine — almost none of it is on the
22
23
  // installRuntimeArtifacts call tree. Only `readInstallManifest` and
@@ -122,8 +123,17 @@ function evaluateRemoveEmptyDir(configDir, fullPath) {
122
123
  catch {
123
124
  return 'left-in-place';
124
125
  }
125
- if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep)) {
126
- // Refuses both "target IS configDir" and "target escaped configDir".
126
+ // `resolvedTarget === resolvedRoot` is a DELIBERATE ADDITIONAL rejection,
127
+ // separate from the containment decision: `tryWithinRootLexical` treats
128
+ // target === root as CONTAINED, but removing the config root itself is
129
+ // never in scope for this action (see the doc comment above) — this arm
130
+ // prevents `rmdirSync` from ever being asked to remove `configDir` itself.
131
+ // Kept as its own check per ADR-4650 decision 6 (a wrapper may add its own
132
+ // conditions on top of the canonical predicate, never invert it).
133
+ if (resolvedTarget === resolvedRoot)
134
+ return 'left-in-place';
135
+ if ((0, security_cjs_1.tryWithinRootLexical)(resolvedTarget, resolvedRoot) === null) {
136
+ // Refuses "target escaped configDir".
127
137
  return 'left-in-place';
128
138
  }
129
139
  let entries;
@@ -598,11 +608,30 @@ function acquireInstallMigrationLock(configDir, { timeoutMs = DEFAULT_LOCK_TIMEO
598
608
  }
599
609
  }
600
610
  }
611
+ // DELIBERATELY LEXICAL — the RESOLUTION policy stays lexical, never realpath
612
+ // (`assertWithinRoot` / `tryWithinRoot`, src/security.cts).
613
+ //
614
+ // Reviewed under epic #4636 Phase 3 and reverted after the remote matrix proved
615
+ // the realpath collapse wrong. This module's contract is that a symlinked
616
+ // managed path is treated AS A LINK and never dereferenced — it is snapshotted
617
+ // as a link, restored as a link, and backed up as a link. The realpath-based
618
+ // predicate dereferences exactly the symlinks this module exists to preserve
619
+ // and then rejects them for escaping configDir
620
+ // ("migration path escapes configDir: extensions/gsd.cjs"). Four tests in
621
+ // tests/installer-migrations.test.cjs pin that behavior.
622
+ //
623
+ // The containment DECISION now routes through the canonical LEXICAL predicate
624
+ // (`tryWithinRootLexical`, ADR-4650 decision 6) — only the comparison moved;
625
+ // the lexical policy itself remains this module's own required choice, and
626
+ // the thrown message / returned `fullPath` are unchanged.
627
+ //
628
+ // `normalizeRelPath` is the pre-gate: it throws on absolute paths and on any
629
+ // '..' segment BEFORE this runs, so the check below is defense-in-depth over
630
+ // already-traversal-free input rather than the primary boundary.
601
631
  function ensureInsideConfig(configDir, relPath) {
602
632
  const normalized = normalizeRelPath(relPath);
603
633
  const fullPath = node_path_1.default.resolve(configDir, normalized);
604
- const root = node_path_1.default.resolve(configDir);
605
- if (fullPath !== root && !fullPath.startsWith(root + node_path_1.default.sep)) {
634
+ if ((0, security_cjs_1.tryWithinRootLexical)(fullPath, configDir) === null) {
606
635
  throw new Error(`migration path escapes configDir: ${relPath}`);
607
636
  }
608
637
  return { normalized, fullPath };
@@ -205,6 +205,40 @@ function output(result, raw, rawValue) {
205
205
  // and skipping process.exit() lets the event loop drain naturally.
206
206
  writeAllSync(1, data);
207
207
  }
208
+ /**
209
+ * Single owner of the "a no-op decline reports the real condition" pairing:
210
+ * a `[gsd-tools] WARNING:` stderr disclosure plus the matching
211
+ * `{ [flagKey]: false, ...computed, reason }` JSON payload (#3957, epic
212
+ * #3473 B9). Before this helper, each CLI command's no-op arm hand-wrote
213
+ * both halves independently, and #3957's own sweep found sites where one
214
+ * half drifted: a decline that discarded values it had already computed, a
215
+ * `reason` string naming a condition that hadn't actually fired, or no
216
+ * stderr disclosure at all (missing the `[gsd-tools] WARNING:` convention
217
+ * #3217, ADR-3180 §7.6 rule 4, established for exactly this situation —
218
+ * a JSON `reason` field alone is easy for a caller piping stdout through
219
+ * `--json` to never read). Routing every no-op decline through one call
220
+ * site makes the convention structural — a call site, not a habit a fifth
221
+ * arm can independently forget — rather than leaving it to per-site
222
+ * discipline.
223
+ *
224
+ * `disclosure` is written to stderr VERBATIM. Like `error()`'s `message`
225
+ * argument (see that function's own doc comment above `formatDiagnosticToken`),
226
+ * this function stays a dumb, faithful writer and does not sanitize it — a
227
+ * caller that interpolates an UNTRUSTED substring (a caller-supplied
228
+ * blocker/session-field string, an argv token) into `disclosure` MUST pass
229
+ * that substring through `formatDiagnosticToken` first. Skipping that step
230
+ * lets an embedded `\n` forge a second, attacker-authored
231
+ * `[gsd-tools] WARNING:` line — the same risk `error()`'s doc comment
232
+ * documents for its own stderr write.
233
+ */
234
+ function declineNoOp(raw, flagKey, reason, disclosure, computed = {}) {
235
+ // `process.stderr.write`, matching the `[gsd-tools] WARNING:` call sites
236
+ // this helper replaces (state.cts's stateReplaceFieldWithFallback and the
237
+ // cmdStateUpdateProgress decline arms) — not the `writeAllSync(2, …)`
238
+ // primitive `error()` uses, which is reserved for the fatal-exit path.
239
+ process.stderr.write(`[gsd-tools] WARNING: ${disclosure}\n`);
240
+ output({ [flagKey]: false, ...computed, reason }, raw, 'false');
241
+ }
208
242
  /**
209
243
  * Frozen enum of typed reason codes used by error() for structured errors.
210
244
  * Each subcommand contributes its own codes; the enum exists so tests can
@@ -392,6 +426,7 @@ module.exports = {
392
426
  ensureGsdTempDir,
393
427
  reapStaleTempFiles,
394
428
  output,
429
+ declineNoOp,
395
430
  serializeForOutput,
396
431
  ERROR_REASON,
397
432
  setJsonErrorMode,