@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
@@ -30,6 +30,9 @@
30
30
  * list-todos [area] Count and enumerate pending todos
31
31
  * list-seeds [status] List captured seeds (optional status filter)
32
32
  * verify-path-exists <path> Check file/directory existence
33
+ * quick-tasks-migrate Migrate STATE.md's "Quick Tasks Completed"
34
+ * table onto the canonical schema (#3730).
35
+ * No-op when absent or already canonical.
33
36
  * quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
34
37
  * Completed" table (schema-backed via
35
38
  * markdown-table.cjs; #2133/ADR-2143).
@@ -334,6 +337,11 @@ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
334
337
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
335
338
  const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
336
339
  const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
340
+ // #3676 (Phase 4, epic #3344): quick-batch is a first-party, always-on
341
+ // command family (like `/gsd:quick`) — wired directly into
342
+ // HOST_COMMAND_ROUTERS, not the opt-in capability-registry/`activationKey`
343
+ // path graphify uses.
344
+ const { routeQuickBatchCommand } = require('./lib/quick-batch-command-router.cjs');
337
345
  const { routeCapabilityCommand } = require('./lib/capability-command-router.cjs');
338
346
  const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-command-router.cjs');
339
347
  const smartEntryMod = require('./lib/smart-entry.cjs');
@@ -430,8 +438,9 @@ function dispatchCapabilityCommand({ command, args, cwd, raw, error, registry, r
430
438
  // Step 2: confinement check — belt-and-suspenders even after the basename
431
439
  // validation above. Resolved path must be inside libDir (not equal to it,
432
440
  // and must start with libDir + sep so "libDir-suffix" can't sneak through).
433
- const resolved = path.resolve(libDir, m);
434
- if (resolved === libDir || !resolved.startsWith(libDir + path.sep)) {
441
+ const { tryWithinRootLexical } = require('./lib/security.cjs');
442
+ const resolved = tryWithinRootLexical(m, libDir);
443
+ if (resolved === null || resolved === path.resolve(libDir)) {
435
444
  throw new Error('capability module path escapes bin/lib/: ' + JSON.stringify(m));
436
445
  }
437
446
  // Step 3: require the resolved absolute path — the SAME representation that
@@ -507,18 +516,19 @@ function defaultRequireFromInstallRoot(installRoot, m) {
507
516
  if (typeof m !== 'string' || !/^[A-Za-z0-9._-]+\.cjs$/.test(m)) {
508
517
  throw new Error('capability module must be a bare .cjs basename: ' + JSON.stringify(m));
509
518
  }
510
- // Realpath the root so a symlinked ancestor can't widen confinement.
511
- const realRoot = fs.realpathSync(installRoot);
512
- const resolved = path.resolve(realRoot, m);
513
- if (resolved === realRoot || !resolved.startsWith(realRoot + path.sep)) {
519
+ const { tryWithinRoot, tryWithinRootLexical, PathAcceptance } = require('./lib/security.cjs');
520
+ // Lexical containment check: a symlinked ancestor can't widen confinement.
521
+ const lexical = tryWithinRootLexical(m, installRoot);
522
+ if (lexical === null || lexical === path.resolve(installRoot)) {
514
523
  throw new Error('capability module path escapes its install root: ' + JSON.stringify(m));
515
524
  }
516
- // The module file itself must not be a symlink pointing outside the root.
517
- const realResolved = fs.realpathSync(resolved);
518
- if (realResolved !== realRoot && !realResolved.startsWith(realRoot + path.sep)) {
525
+ // Realpath/symlink check: the module file itself must not be a symlink
526
+ // pointing outside the root.
527
+ const real = tryWithinRoot(m, installRoot, PathAcceptance.RelativeOnly);
528
+ if (real === null) {
519
529
  throw new Error('capability module resolves outside its install root (symlink): ' + JSON.stringify(m));
520
530
  }
521
- return require(realResolved);
531
+ return require(real);
522
532
  }
523
533
 
524
534
  /**
@@ -776,7 +786,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
776
786
  error,
777
787
  output: output,
778
788
  });
779
- if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
789
+ if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw, { dryRun: args.includes('--dry-run') });
780
790
  }
781
791
 
782
792
  function routeConfigSetModelProfile({ args, cwd, raw }) {
@@ -942,15 +952,27 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
942
952
  function routeCommit({ args, cwd, raw, error }) {
943
953
  const amend = args.includes('--amend');
944
954
  const noVerify = args.includes('--no-verify');
945
- const filesIndex = args.indexOf('--files');
955
+ // #4208: `--files` and `--files-removed` are two path lists, each
956
+ // running from its flag to the NEXT LIST FLAG. A boolean flag
957
+ // inside a list (`--files a --amend b`) is skipped, not a
958
+ // terminator: that is what the previous slice-to-end collection
959
+ // did (it filtered `--` tokens and kept everything else), and a
960
+ // list that stopped at any `--` token silently dropped `b`
961
+ // (review of #4253). The previous form could not carry a second
962
+ // list flag at all, which is the only thing that changed.
963
+ // A REPEATED list flag (`--files a --files b`) merges, as the old
964
+ // slice-to-end parse merged it: every occurrence contributes its
965
+ // run, and none of them ends another's silently.
966
+ const firstListFlag = args.findIndex((a, i) => i > 0 && COMMIT_LIST_FLAGS.has(a));
946
967
  // Collect all positional args between command name and first flag,
947
968
  // then join them — handles both quoted ("multi word msg") and
948
969
  // unquoted (multi word msg) invocations from different shells
949
- const endIndex = filesIndex !== -1 ? filesIndex : args.length;
970
+ const endIndex = firstListFlag !== -1 ? firstListFlag : args.length;
950
971
  const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
951
972
  const message = messageArgs.join(' ') || undefined;
952
- const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
953
- commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
973
+ const files = collectListFlagValues(args, '--files');
974
+ const filesRemoved = collectListFlagValues(args, '--files-removed');
975
+ commands.cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved);
954
976
  }
955
977
 
956
978
  function routeCheckCommit({ args, cwd, raw, error }) {
@@ -1177,6 +1199,34 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1177
1199
  commands.cmdVerifyPathExists(cwd, args[1], raw);
1178
1200
  }
1179
1201
 
1202
+ function routeQuickTasksMigrate({ cwd, raw }) {
1203
+ // #3730 (option b, maintainer decision 2026-09-02): the supported repair
1204
+ // path for a legacy Quick Tasks table GSD's own pre-registry prose created.
1205
+ // Silent no-op when the section is absent or the table is already canonical
1206
+ // — the quick/fast workflows call this before their first append, so the
1207
+ // migration runs exactly once, on the first quick run, unprompted otherwise.
1208
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
1209
+ if (!fs.existsSync(statePath)) {
1210
+ output({ ok: true, migrated: false, reason: `STATE.md not found at ${statePath}` }, raw);
1211
+ return;
1212
+ }
1213
+ const { migrateQuickTasksTable } = require('./lib/markdown-table.cjs');
1214
+ let report;
1215
+ state.readModifyWriteStateMd(statePath, (content) => {
1216
+ const result = migrateQuickTasksTable(content);
1217
+ if (!result.ok) {
1218
+ throw new ExitError(1, `⚠ quick-tasks-migrate: ${result.reason}`);
1219
+ }
1220
+ report = result.value;
1221
+ return result.value.content;
1222
+ }, cwd, { resync: false });
1223
+ output({
1224
+ ok: true,
1225
+ migrated: report.migrated,
1226
+ ...(report.migrated ? { from: report.from, rows: report.rows } : {}),
1227
+ }, raw);
1228
+ }
1229
+
1180
1230
  function routeQuickTasksAppend({ args, cwd, raw, error }) {
1181
1231
  // #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
1182
1232
  // `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
@@ -1286,7 +1336,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1286
1336
  const fsx = require('node:fs');
1287
1337
  const os = require('node:os');
1288
1338
  const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
1289
- const { resolveLanePlan } = require('./lib/review-lane-invocation.cjs');
1339
+ const { resolveLanePlan, resolveLaneEffort, resolveLaneBudget } = require('./lib/review-lane-invocation.cjs');
1340
+ const modelCatalog = require('./lib/model-catalog.cjs');
1290
1341
  const runner = require('./lib/review-lane-runner.cjs');
1291
1342
  const cfgLoader = require('./lib/config-loader.cjs');
1292
1343
  const capabilityLoader = require('./lib/capability-loader.cjs');
@@ -1298,18 +1349,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1298
1349
  const sub = args[1];
1299
1350
  // Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
1300
1351
  // to the `sub !== 'invoke'` usage-error branch far below (after loading the
1301
- // capability registry AND building a per-lane plan for every lane — which itself
1302
- // spawns one child `query resolve-execution` process per lane via `effortFor`,
1303
- // up to 12 subprocess spawns for the default lane set) before ever reporting the
1304
- // error. That made an invalid subcommand slow instead of instant, and under bench
1305
- // load (many sequential node spawns) `review-lane bogus` could exceed a caller's
1306
- // spawn timeout and be killed before writing anything to stderr — the CI-observed
1307
- // failure was empty stdout AND stderr, not the expected usage message (#3148).
1352
+ // capability registry AND building a per-lane plan for every lane) before ever
1353
+ // reporting the error. That made an invalid subcommand slow instead of instant, and
1354
+ // under bench load `review-lane bogus` could exceed a caller's spawn timeout and be
1355
+ // killed before writing anything to stderr — the CI-observed failure was empty stdout
1356
+ // AND stderr, not the expected usage message (#3148). The plan path used to be far
1357
+ // heavier still: it spawned one child `query resolve-execution` process PER LANE to
1358
+ // fetch effort, up to 12 subprocess spawns for the default set. #4255 resolves effort
1359
+ // in-process from the lane's own declaration, so that cost is gone; the fail-fast
1360
+ // check stays because building 12 plans is still work an unrecognized sub should skip.
1308
1361
  // `plan`/`invoke` are the only subs that need the expensive plan-building path
1309
1362
  // below; `sections`/`flags` return earlier still. Anything else errors here, before
1310
1363
  // any of that work starts.
1311
- if (!['plan', 'invoke', 'sections', 'flags'].includes(sub)) {
1312
- error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
1364
+ if (!['plan', 'invoke', 'sections', 'flags', 'dispatch-step', 'explicit-from-argv'].includes(sub)) {
1365
+ error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
1313
1366
  return;
1314
1367
  }
1315
1368
  const runDir = flag('--run-dir') || '.';
@@ -1331,6 +1384,86 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1331
1384
  return cur;
1332
1385
  };
1333
1386
 
1387
+ // Shared by `invoke` and `dispatch-step` (#4209) — both need the SAME bounded
1388
+ // spawn/http/fs seam `runLane` requires (RunnerDeps). Factored out so the two
1389
+ // callers can never disagree about how a lane's binary is resolved or how its
1390
+ // process is bounded; a fix to either reaches both.
1391
+ const buildLaneRunnerDeps = () => ({
1392
+ spawn: (binary, argv, opts) => {
1393
+ // #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
1394
+ // as .cmd shims. spawnSync with a bare name + shell:false fails with
1395
+ // ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
1396
+ // gate used in runWithTimeout: detect .cmd/.bat and mediate through
1397
+ // cmd.exe /d /s /c with an explicit argv array (no shell:true).
1398
+ //
1399
+ // #3275: descriptors declare BARE names, so the gate above never saw an
1400
+ // extension — resolve through the shared PATH+PATHEXT resolver FIRST
1401
+ // (the same one `hasBinary` uses, so probe and spawn can never disagree
1402
+ // about what the lane's binary is). POSIX keeps the bare name: Node's own
1403
+ // PATH search already worked there, and the #3275 acceptance contract
1404
+ // holds macOS/Linux behavior unchanged. A name that resolves to nothing
1405
+ // falls back to the declared name so the ENOENT still surfaces (#3086).
1406
+ // #3411: the resolve-then-mediate pair is one seam call now. Both halves had
1407
+ // private copies here; `projectSpawnInvocation` owns them, so a fix to either
1408
+ // reaches every spawn site instead of only this one.
1409
+ //
1410
+ // Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
1411
+ // binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
1412
+ // from the same resolver, so probe and spawn must agree on the exact file.
1413
+ const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
1414
+ const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
1415
+ const r = cp.spawnSync(spawnBinary, spawnArgv, {
1416
+ input: opts.input,
1417
+ encoding: 'utf8',
1418
+ timeout: opts.timeoutMs,
1419
+ killSignal: 'SIGKILL',
1420
+ maxBuffer: 64 * 1024 * 1024,
1421
+ shell: false, // argv array only — never a shell string (no interpolation of config values)
1422
+ // #2483: a lane's declared env pairs merged OVER this process's environment, for this
1423
+ // child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
1424
+ // into the orchestrating session or into the next lane.
1425
+ ...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
1426
+ ...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1427
+ });
1428
+ return {
1429
+ status: r.status,
1430
+ stdout: r.stdout || '',
1431
+ stderr: r.stderr || '',
1432
+ errorCode: r.error && r.error.code ? r.error.code : undefined,
1433
+ };
1434
+ },
1435
+ httpJson: async (url, opts) => {
1436
+ try {
1437
+ const res = await fetch(url, {
1438
+ method: opts.method,
1439
+ headers: opts.body ? { 'Content-Type': 'application/json' } : undefined,
1440
+ body: opts.body,
1441
+ signal: AbortSignal.timeout(opts.timeoutMs),
1442
+ });
1443
+ return { ok: res.ok, status: res.status, body: await res.text() };
1444
+ } catch (e) {
1445
+ return { ok: false, status: 0, body: '', error: e && e.message ? e.message : String(e) };
1446
+ }
1447
+ },
1448
+ readFile: (p) => fsx.readFileSync(p, 'utf8'),
1449
+ writeFile: (p, c) => fsx.writeFileSync(p, c, 'utf8'),
1450
+ exists: (p) => fsx.existsSync(p),
1451
+ // PATH scan rather than spawning `command -v` / `where`. Two reasons: it spawns nothing at
1452
+ // all (a probe that costs a process is a probe you avoid running, which is how the original
1453
+ // Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
1454
+ // Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
1455
+ //
1456
+ // #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
1457
+ // above. Two private copies of "what is this declared binary?" is how the
1458
+ // defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
1459
+ // so a lane reported available for a spawn that could never start. One
1460
+ // resolver, both seams — if one changes, the other changes with it.
1461
+ hasBinary: (name) => resolveSpawnBinary(name) !== null,
1462
+ configGet,
1463
+ homeDir: os.homedir(),
1464
+ warn: (m) => process.stderr.write(`${m}\n`),
1465
+ });
1466
+
1334
1467
  const selected = (flag('--selected') || '')
1335
1468
  .split(',').map((s) => s.trim()).filter(Boolean);
1336
1469
  // ADR-2782 D8 (#2927): the lane map is first-party ∪ INSTALLED overlay
@@ -1358,6 +1491,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1358
1491
  const laneBySlug = new Map(mergedLanes.map((l) => [l.slug, l]));
1359
1492
  const chosen = selected.length ? selected : mergedLanes.map((l) => l.slug);
1360
1493
 
1494
+ // #4209 RQ-02: match a workflow's raw CLI argv (e.g. `--codex`, `--agy`) against the SAME
1495
+ // merged first-party+installed-overlay roster this whole function already built above,
1496
+ // instead of a workflow re-deriving its own copy of `loadRegistry`/`mergeReviewerLanes` via
1497
+ // an inline `node -e` (a rename-only duplicate of the block starting at `mergedLanes =
1498
+ // REVIEWER_LANES` above — `code-review-flags.cjs`'s own header states "this is the canonical
1499
+ // flag-parsing surface — do not replicate inline bash parsing" for exactly this reason).
1500
+ // Everything after `--` is a candidate flag; matched lane slugs print sorted and comma-joined.
1501
+ if (sub === 'explicit-from-argv') {
1502
+ const sepIdx = args.indexOf('--');
1503
+ const candidateArgs = new Set(sepIdx === -1 ? [] : args.slice(sepIdx + 1));
1504
+ const slugs = [];
1505
+ for (const lane of mergedLanes) {
1506
+ const flags = Array.isArray(lane.flags) ? lane.flags : [];
1507
+ if (flags.some((f) => candidateArgs.has(f))) slugs.push(lane.slug);
1508
+ }
1509
+ process.stdout.write([...new Set(slugs)].sort().join(','));
1510
+ return;
1511
+ }
1512
+
1361
1513
  if (sub === 'sections') {
1362
1514
  const rows = chosen
1363
1515
  .map((s) => laneBySlug.get(s))
@@ -1391,67 +1543,159 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1391
1543
  return;
1392
1544
  }
1393
1545
 
1394
- // Effort argv is resolved per lane by the host's own execution policy, through the SAME
1395
- // `resolve-execution` surface the bash legs used (`--host <slug>`), so the host's negotiated
1396
- // effortSurface still decides whether an argument is emitted and the catalog still owns the
1397
- // syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
1398
- // stdout and exits, so it cannot be called in-process for a value — this spawns the same
1399
- // bounded query the legs did, once per selected lane. A lane whose slug is not a known host
1400
- // resolves to no effort argument at all.
1546
+ // Effort argv is resolved from the LANE's own review configuration (#4255), then rendered
1547
+ // through the host's negotiated `effortSurface` so ADR-1239/#2481's trust-boundary invariant
1548
+ // still decides whether an argument is emitted at all and the catalog still owns the syntax.
1549
+ //
1550
+ // What this replaced: a `query resolve-execution gsd-plan-checker --host <slug>` spawn per
1551
+ // lane. The agent id was a hardcoded literal, so the `--host` argument chose only the argv
1552
+ // RENDERING while the LEVEL always came from the installed plan-checker's frontmatter — `low`
1553
+ // under every shipped model profile. Every prompt-fed reviewer therefore ran at a fast
1554
+ // structural verifier's effort, and because the rendered argument is a CLI config override it
1555
+ // silently beat the effort the operator had configured for that CLI. At `low` a large
1556
+ // source-grounded prompt makes a model end its turn with no final message, so the lane came
1557
+ // back empty and the stub read as a crash.
1401
1558
  //
1402
- // NOT `--raw` and NOT `--pick` (#2295). `--raw` prints only the resolved EFFORT ('low') with
1403
- // no host-specific rendering at all. `--pick effort_argv_string` used to be the answer — the
1404
- // rendered array re-joined into a string ('-c model_reasoning_effort=low') — but the caller
1405
- // then had to `.split(/\s+/)` that string back apart to get an argv array, and re-splitting a
1406
- // string the callee just joined is a lossy round trip: any argv element that legitimately
1407
- // contains a space would come back split into two argv elements, corrupting the very argv it
1408
- // was rendered to preserve. Reading the UNPICKED object instead gives both `effort_argv` (a
1409
- // real string array, used verbatim, no re-splitting) and `effort_argv_value` (the bare level,
1410
- // #2295's `plan.effort`) from the one spawn.
1411
- const EMPTY_EFFORT = { argv: [], value: null };
1412
- const effortFor = (slug) => {
1559
+ // `resolveLaneEffort` is pure and lives beside the other lane resolution; this closure only
1560
+ // injects the rendering, which needs the registry and the catalog.
1561
+ const renderLaneEffort = (host, level) => {
1413
1562
  try {
1414
- const r = cp.spawnSync(
1415
- process.execPath,
1416
- [__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
1417
- { cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
1418
- );
1419
- if (r.status !== 0) return EMPTY_EFFORT;
1420
- let parsed;
1421
- try {
1422
- parsed = JSON.parse(String(r.stdout || ''));
1423
- } catch { return EMPTY_EFFORT; }
1424
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
1425
- const argv = Array.isArray(parsed.effort_argv)
1426
- ? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
1427
- : [];
1428
- const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
1429
- ? parsed.effort_argv_value
1430
- : null;
1431
- return { argv, value };
1432
- } catch { return EMPTY_EFFORT; }
1563
+ const surface = commands.effortSurfaceForHost(cwd, host);
1564
+ const r = modelCatalog.renderEffortArgv(host, level, surface);
1565
+ return { argv: Array.isArray(r && r.argv) ? r.argv : [], value: (r && r.value) || null };
1566
+ } catch { return { argv: [], value: null }; }
1433
1567
  };
1568
+ const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
1569
+
1570
+ // Per-lane prompt budget: `resolveLaneBudget` (review-lane-invocation.cjs) owns the #2797
1571
+ // resolution semantics (shared with src/reviewer-step-dispatch.cts, #4209 R3 — was two
1572
+ // verbatim copies). Only the budget VALUE is resolved here; assembly/trimming stay in
1573
+ // `prompt-budget`, which the workflow calls, handing the trimmed file back via `--prompt-file`.
1574
+ const budgetFor = (lane) => resolveLaneBudget(lane, configGet);
1575
+
1576
+ // #4209 (ADR-2782 seam) — the ONE interpreter route for a step that declared
1577
+ // `supportsReviewerLanes: true`. Wires `dispatchReviewerLanes` (src/reviewer-step-dispatch.cts)
1578
+ // to the SAME plan/invoke machinery `plan`/`invoke` above use, so a step opting in gets exact
1579
+ // parity with hand-driven `review-lane plan|invoke` rather than a second implementation.
1580
+ // Canonical file paths travel on stdin, never argv (see gsd-core/workflows/code-review.md's
1581
+ // "Files travel on stdin" note) — a 50+-file scope with long paths approaches the Windows
1582
+ // execFileSync argv ceiling, and stdin has no such bound.
1583
+ //
1584
+ // Returns EARLY, like `sections`/`flags` above, rather than falling into the `plans` builder
1585
+ // below: that builder spawns one `effortFor` child process PER LANE IN THE ROSTER (chosen
1586
+ // defaults to every merged lane when nothing is selected), which would burn ~12 wasted spawns
1587
+ // on every dispatch-step call whether or not anything was actually selected. `dispatchReviewerLanes`
1588
+ // builds its own per-SELECTED-lane plan below instead, bounded by the (typically 0-3) explicitly
1589
+ // requested slugs, not the whole roster.
1590
+ if (sub === 'dispatch-step') {
1591
+ const { dispatchReviewerLanes } = require('./lib/reviewer-step-dispatch.cjs');
1592
+ const explicitFlags = (flag('--explicit') || '').split(',').map((s) => s.trim()).filter(Boolean);
1593
+ const depth = flag('--depth') || '';
1594
+ const baseSha = flag('--base-sha') || '';
1595
+ // #4209: this command IS the reusable capability/step-dispatch trait check — see
1596
+ // gsd-core/references/loop-hook-dispatch.md for what supportsReviewerLanes means and why
1597
+ // this is the one place it's resolved. Calls the SAME resolver `loop render-hooks` uses,
1598
+ // `resolveActiveHooksForPoint`, directly in-process — no subprocess, no JSON re-parse, and
1599
+ // no exposure to `io.cjs`'s `@file:` overflow protocol (which only applies to the
1600
+ // rendered-string envelope this path never touches).
1601
+ const capId = flag('--cap-id') || '';
1602
+ const point = flag('--point') || '';
1603
+ let trait = false;
1604
+ if (capId && point) {
1605
+ try {
1606
+ const { resolveActiveHooksForPoint } = loopResolver;
1607
+ const { activeHooks } = resolveActiveHooksForPoint(cwd, point);
1608
+ trait = activeHooks.some((h) => h && h.capId === capId && h.supportsReviewerLanes === true);
1609
+ } catch (e) {
1610
+ process.stderr.write(`Warning: reviewer-lane trait resolution failed for --cap-id ${capId} --point ${point}: ${e && e.message ? e.message : String(e)} — treating as not enabled.\n`);
1611
+ trait = false;
1612
+ }
1613
+ } else if (capId || point) {
1614
+ // #4209 RQ-03: exactly one of the two was passed — a caller with NO capability-step
1615
+ // context at all (neither flag) is the legitimate, silent no-op documented above, but a
1616
+ // caller that named a capability without its point (or vice versa) is misconfigured, not
1617
+ // opted out, and that must not look identical to a correct opt-out on the wire.
1618
+ process.stderr.write(`Warning: --cap-id and --point must both be given to resolve the reviewer-lane trait (got --cap-id=${JSON.stringify(capId)} --point=${JSON.stringify(point)}) — treating as not enabled.\n`);
1619
+ }
1620
+ // No piped stdin (interactive TTY): fail closed to empty paths instead of blocking
1621
+ // indefinitely on a TTY EOF the caller never sends.
1622
+ let stdinPaths = '';
1623
+ if (!process.stdin.isTTY) {
1624
+ try { stdinPaths = fsx.readFileSync(0, 'utf8'); } catch { stdinPaths = ''; }
1625
+ }
1626
+ const paths = stdinPaths.split('\n').map((s) => s.trim()).filter(Boolean);
1627
+
1628
+ // Reuse the exact effort-aware, per-lane plan `plan` builds above (DISP-03: "planned
1629
+ // through the existing `review-lane plan` interface") rather than the interpreter's
1630
+ // simpler default plan callback, which does not resolve per-host effort.
1631
+ const planFn = (lane, ctx) => {
1632
+ const effort = effortFor(lane.slug);
1633
+ return resolveLanePlan({
1634
+ lane, configGet: ctx.configGet, runDir: ctx.runDir, repoRoot: ctx.repoRoot,
1635
+ effortArgs: effort.argv, effortValue: effort.value,
1636
+ });
1637
+ };
1434
1638
 
1435
- /**
1436
- * Per-lane prompt budget (#2797 semantics, preserved exactly).
1437
- *
1438
- * `-1` is the UNSET sentinel and falls back to the central `review.max_prompt_tokens`, because
1439
- * `0` is a legitimate value meaning "do not trim this lane". Treating 0 as unset would silently
1440
- * switch a user who deliberately disabled trimming onto the global budget.
1441
- *
1442
- * Only the budget VALUE is resolved here. Assembly and trimming stay in `prompt-budget`, which
1443
- * already owns that machinery and is already tested; the workflow calls it and hands the
1444
- * trimmed file back via `--prompt-file`. Re-implementing it inside the runner would fork a
1445
- * tested surface for no gain.
1446
- */
1447
- const budgetFor = (lane) => {
1448
- if (!lane.promptBudgetKey) return null;
1449
- const per = configGet(lane.promptBudgetKey);
1450
- const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
1451
- if (isNum(per) && per !== -1) return per;
1452
- const global = configGet('review.max_prompt_tokens');
1453
- return isNum(global) ? global : null;
1454
- };
1639
+ const runnerDeps = buildLaneRunnerDeps();
1640
+ const invokeFn = async (lane, plan) => {
1641
+ let consentedHost;
1642
+ if (plan.transport === 'openai-http') {
1643
+ try {
1644
+ const consent = require('./lib/capability-consent.cjs');
1645
+ const projectRoot = require('./lib/project-root.cjs').consentProjectRoot(cwd);
1646
+ const capId = String(lane.slug).replace(/_/g, '-');
1647
+ consentedHost = consent.readConsentedReviewerHost({ projectRoot, id: capId });
1648
+ } catch { consentedHost = undefined; }
1649
+ }
1650
+ // DISP-04/05: every selected lane is invoked through this SAME `runner.runLane` seam
1651
+ // `invoke` uses, exactly once (the interpreter's own for-loop over `selection.selected`
1652
+ // never revisits a slug).
1653
+ return runner.runLane(plan, runnerDeps, { consentedHost, explicitlyRequested: true, repoRoot });
1654
+ };
1655
+
1656
+ // `resolveReviewerSelection` only selects an explicit flag present in `detected`
1657
+ // (ADR-2782 D4: absent-safe governs discovery, never explicit selection — a slug the
1658
+ // roster does not declare is rejected here as an explicit-selection error). REAL
1659
+ // host availability (is the CLI actually installed?) is a separate, already-owned
1660
+ // check inside `runner.runLane`'s `probeLane` at invoke time below — duplicating a
1661
+ // second `command -v` probe here would let the two disagree about what "available"
1662
+ // means, which is the exact defect class `resolveSpawnBinary` was consolidated to
1663
+ // prevent (#3275).
1664
+ //
1665
+ // GUARDED ON explicitFlags.length, not unconditional: `resolveReviewerSelection`'s
1666
+ // precedence chain (explicit > --all > review.default_reviewers > all detected) ends,
1667
+ // when none of the first three apply, in `selected = [...detected]` — the SAME
1668
+ // "no flags means every detected reviewer" default `/gsd:review` intentionally uses.
1669
+ // Source review's COMP-01 contract is the opposite: no reviewer-lane flag means inert,
1670
+ // unchanged from before #4209. Passing a non-empty
1671
+ // `detected` unconditionally would silently opt every dispatch-step call with no
1672
+ // `--explicit` into planning+invoking the WHOLE roster via that fallback branch. An
1673
+ // empty `detected` when nothing was asked for makes that fallback resolve to
1674
+ // `[...[]]` = `[]`, so `dispatchReviewerLanes` hits its own `NO_LANES_SELECTED`
1675
+ // early-return before any plan/invoke call — the same fast, inert no-op the caller
1676
+ // gets from an absent `supportsReviewerLanes` trait.
1677
+ const rosterSlugs = explicitFlags.length > 0 ? [...laneBySlug.keys()] : [];
1678
+
1679
+ const dispatchResult = await dispatchReviewerLanes(
1680
+ {
1681
+ trait,
1682
+ selection: { explicitFlags, detected: rosterSlugs },
1683
+ repoRoot,
1684
+ paths,
1685
+ depth,
1686
+ baseSha,
1687
+ runDir,
1688
+ },
1689
+ {
1690
+ getLane: (slug) => laneBySlug.get(slug),
1691
+ configGet,
1692
+ plan: planFn,
1693
+ invoke: invokeFn,
1694
+ },
1695
+ );
1696
+ output(dispatchResult, raw);
1697
+ return;
1698
+ }
1455
1699
 
1456
1700
  const plans = chosen.map((slug) => {
1457
1701
  const lane = laneBySlug.get(slug);
@@ -1462,7 +1706,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1462
1706
  // so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
1463
1707
  let r;
1464
1708
  try {
1465
- const effort = effortFor(slug);
1709
+ const effort = effortFor(lane);
1466
1710
  r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
1467
1711
  } catch (e) {
1468
1712
  return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
@@ -1486,7 +1730,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1486
1730
  }
1487
1731
 
1488
1732
  if (sub !== 'invoke') {
1489
- error("Usage: review-lane <plan|invoke|sections|flags> [--selected a,b] [--run-dir D] [--repo-root R]");
1733
+ error("Usage: review-lane <plan|invoke|sections|flags|dispatch-step|explicit-from-argv> [--selected a,b] [--run-dir D] [--repo-root R]");
1490
1734
  return;
1491
1735
  }
1492
1736
 
@@ -1500,81 +1744,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1500
1744
 
1501
1745
  // EVERY spawn bounded — `DEFECT.UNBOUNDED-SUBPROCESS` (CONTEXT.md:772). A frozen sync spawn
1502
1746
  // cannot be interrupted by --test-force-exit and hangs a whole CI chunk to its 10-minute kill.
1503
- const deps = {
1504
- spawn: (binary, argv, opts) => {
1505
- // #3086: on Windows, reviewer CLIs (gemini, codex, etc.) are installed
1506
- // as .cmd shims. spawnSync with a bare name + shell:false fails with
1507
- // ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
1508
- // gate used in runWithTimeout: detect .cmd/.bat and mediate through
1509
- // cmd.exe /d /s /c with an explicit argv array (no shell:true).
1510
- //
1511
- // #3275: descriptors declare BARE names, so the gate above never saw an
1512
- // extension — resolve through the shared PATH+PATHEXT resolver FIRST
1513
- // (the same one `hasBinary` uses, so probe and spawn can never disagree
1514
- // about what the lane's binary is). POSIX keeps the bare name: Node's own
1515
- // PATH search already worked there, and the #3275 acceptance contract
1516
- // holds macOS/Linux behavior unchanged. A name that resolves to nothing
1517
- // falls back to the declared name so the ENOENT still surfaces (#3086).
1518
- // #3411: the resolve-then-mediate pair is one seam call now. Both halves had
1519
- // private copies here; `projectSpawnInvocation` owns them, so a fix to either
1520
- // reaches every spawn site instead of only this one.
1521
- //
1522
- // Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
1523
- // binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
1524
- // from the same resolver, so probe and spawn must agree on the exact file.
1525
- const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
1526
- const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
1527
- const r = cp.spawnSync(spawnBinary, spawnArgv, {
1528
- input: opts.input,
1529
- encoding: 'utf8',
1530
- timeout: opts.timeoutMs,
1531
- killSignal: 'SIGKILL',
1532
- maxBuffer: 64 * 1024 * 1024,
1533
- shell: false, // argv array only — never a shell string (no interpolation of config values)
1534
- // #2483: a lane's declared env pairs merged OVER this process's environment, for this
1535
- // child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
1536
- // into the orchestrating session or into the next lane.
1537
- ...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
1538
- ...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1539
- });
1540
- return {
1541
- status: r.status,
1542
- stdout: r.stdout || '',
1543
- stderr: r.stderr || '',
1544
- errorCode: r.error && r.error.code ? r.error.code : undefined,
1545
- };
1546
- },
1547
- httpJson: async (url, opts) => {
1548
- try {
1549
- const res = await fetch(url, {
1550
- method: opts.method,
1551
- headers: opts.body ? { 'Content-Type': 'application/json' } : undefined,
1552
- body: opts.body,
1553
- signal: AbortSignal.timeout(opts.timeoutMs),
1554
- });
1555
- return { ok: res.ok, status: res.status, body: await res.text() };
1556
- } catch (e) {
1557
- return { ok: false, status: 0, body: '', error: e && e.message ? e.message : String(e) };
1558
- }
1559
- },
1560
- readFile: (p) => fsx.readFileSync(p, 'utf8'),
1561
- writeFile: (p, c) => fsx.writeFileSync(p, c, 'utf8'),
1562
- exists: (p) => fsx.existsSync(p),
1563
- // PATH scan rather than spawning `command -v` / `where`. Two reasons: it spawns nothing at
1564
- // all (a probe that costs a process is a probe you avoid running, which is how the original
1565
- // Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
1566
- // Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
1567
- //
1568
- // #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
1569
- // above. Two private copies of "what is this declared binary?" is how the
1570
- // defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
1571
- // so a lane reported available for a spawn that could never start. One
1572
- // resolver, both seams — if one changes, the other changes with it.
1573
- hasBinary: (name) => resolveSpawnBinary(name) !== null,
1574
- configGet,
1575
- homeDir: os.homedir(),
1576
- warn: (m) => process.stderr.write(`${m}\n`),
1577
- };
1747
+ const deps = buildLaneRunnerDeps();
1578
1748
 
1579
1749
  // ADR-1517 reviewer instances resolve THROUGH a lane rather than being lanes themselves
1580
1750
  // (ADR-2782 D8), so they reuse this seam with three substitutions instead of duplicating the
@@ -1622,7 +1792,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1622
1792
  `model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
1623
1793
  );
1624
1794
  }
1625
- const instanceEffort = effortFor(entry.slug);
1795
+ const instanceEffort = effortFor(lane);
1626
1796
  const overridden = resolveLanePlan({
1627
1797
  lane,
1628
1798
  configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
@@ -1886,6 +2056,112 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
1886
2056
  }
1887
2057
  }
1888
2058
 
2059
+ /**
2060
+ * #3673 (ADR-1239 Phase 1) — typed, PURE-READ query exposing the negotiated
2061
+ * `dispatch.maxConcurrency` numeric sub-field. Sibling of `dispatch-isolation`
2062
+ * above, but — per the #3673 design doc's explicit rejection of a write path
2063
+ * for this query — writes NOTHING: no sentinel file, no side effect at all.
2064
+ * A read-only capacity lookup for a later (Phase 4) scheduler to consult.
2065
+ *
2066
+ * Precedence:
2067
+ * 1. GSD_DISPATCH_MAX_CONCURRENCY, if it parses as a positive safe
2068
+ * integer → source: "live". A malformed value (non-numeric, zero,
2069
+ * negative, fractional) is treated exactly as if the variable were
2070
+ * unset — it falls through to tier 2, never errors.
2071
+ * 2. registry.runtimes[id].runtime.hostIntegration.dispatch.maxConcurrency,
2072
+ * if it is a positive safe integer → source: "descriptor". The
2073
+ * "undocumented" sentinel and any other malformed shape fall through
2074
+ * to tier 3.
2075
+ * 3. 1 (the fail-closed floor) → source: "fallback".
2076
+ *
2077
+ * `isPositiveSafeInteger`, required below from `./lib/host-integration.cjs`,
2078
+ * is the SAME exported predicate src/host-integration.cts's
2079
+ * negotiateHostCapabilities applies to the descriptor value — applied
2080
+ * identically to both the env value and the descriptor value here so the
2081
+ * two tiers can never silently disagree on what counts as valid.
2082
+ *
2083
+ * `reason` names why the WINNING tier (or the fallback) was chosen: "ok" on
2084
+ * the happy path (live or descriptor honored), else "missing" (descriptor
2085
+ * omitted the field), "undocumented" (descriptor sentinel), "non_integer"
2086
+ * (fractional or NaN), "unsafe_integer" (integer but outside
2087
+ * Number.isSafeInteger's range), "non_positive" (zero or negative),
2088
+ * "non_numeric" (string/object/array/null/boolean), or "unknown_runtime"
2089
+ * (no registry entry for the resolved runtime id — resolution itself never
2090
+ * throws; failure fails closed to the same fallback tier).
2091
+ *
2092
+ * Output:
2093
+ * --raw → prints exactly one positive integer, nothing else
2094
+ * --json → prints { runtime, capacity, declared, source, reason }
2095
+ * default → same as --raw
2096
+ */
2097
+ function routeDispatchCapacity({ args, cwd, raw }) {
2098
+ const { UNDOCUMENTED, isPositiveSafeInteger } = require('./lib/host-integration.cjs');
2099
+
2100
+ let runtimeId = null;
2101
+ let runtimeEntry = null;
2102
+ try {
2103
+ const { resolveRuntime } = require('./lib/runtime-slash.cjs');
2104
+ runtimeId = resolveRuntime(cwd);
2105
+ const registry = require('./lib/capability-registry.cjs');
2106
+ runtimeEntry = registry.runtimes != null ? registry.runtimes[runtimeId] : null;
2107
+ } catch {
2108
+ runtimeEntry = null;
2109
+ }
2110
+ const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.maxConcurrency ?? null;
2111
+
2112
+ let liveValue = null;
2113
+ const rawEnv = process.env.GSD_DISPATCH_MAX_CONCURRENCY;
2114
+ if (typeof rawEnv === 'string' && rawEnv.trim().length > 0) {
2115
+ const parsed = Number(rawEnv.trim());
2116
+ if (isPositiveSafeInteger(parsed)) {
2117
+ liveValue = parsed;
2118
+ }
2119
+ }
2120
+
2121
+ let capacity;
2122
+ let source;
2123
+ let reason;
2124
+ if (liveValue !== null) {
2125
+ capacity = liveValue;
2126
+ source = 'live';
2127
+ reason = 'ok';
2128
+ } else if (runtimeEntry == null) {
2129
+ capacity = 1;
2130
+ source = 'fallback';
2131
+ reason = 'unknown_runtime';
2132
+ } else if (declared === UNDOCUMENTED) {
2133
+ capacity = 1;
2134
+ source = 'fallback';
2135
+ reason = 'undocumented';
2136
+ } else if (isPositiveSafeInteger(declared)) {
2137
+ capacity = declared;
2138
+ source = 'descriptor';
2139
+ reason = 'ok';
2140
+ } else if (declared === null || declared === undefined) {
2141
+ capacity = 1;
2142
+ source = 'fallback';
2143
+ reason = 'missing';
2144
+ } else if (typeof declared !== 'number') {
2145
+ capacity = 1;
2146
+ source = 'fallback';
2147
+ reason = 'non_numeric';
2148
+ } else if (!Number.isSafeInteger(declared)) {
2149
+ capacity = 1;
2150
+ source = 'fallback';
2151
+ reason = Number.isInteger(declared) ? 'unsafe_integer' : 'non_integer';
2152
+ } else {
2153
+ capacity = 1;
2154
+ source = 'fallback';
2155
+ reason = 'non_positive';
2156
+ }
2157
+
2158
+ if (args.indexOf('--json') !== -1) {
2159
+ output({ runtime: runtimeId, capacity, declared, source, reason }, raw);
2160
+ } else {
2161
+ process.stdout.write(String(capacity));
2162
+ }
2163
+ }
2164
+
1889
2165
  // #3714 follow-up — the dispatch seam gated only on PRESENCE of an explicit
1890
2166
  // pin, never on its VALUE, so an Anthropic-flavored global default
1891
2167
  // (~/.gsd/defaults.json model_overrides["gsd-executor"] = "sonnet"/"opus"/
@@ -2571,7 +2847,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
2571
2847
  function routeTodo({ args, cwd, raw, error }) {
2572
2848
  const subcommand = args[1];
2573
2849
  if (subcommand === 'complete') {
2574
- commands.cmdTodoComplete(cwd, args[2], raw);
2850
+ // #4096: this verb's flag surface is closed. `--dry-run` is parsed
2851
+ // and plumbed (mirroring routeMilestone / #2118), and any OTHER
2852
+ // flag fails loudly instead of being silently ignored — an
2853
+ // accepted-but-ignored safety flag converts a deliberate preview
2854
+ // into the mutation it was meant to avoid. (`--raw` is spliced by
2855
+ // the dispatcher before routing; it stays in the set as a guard
2856
+ // against that splice ever moving.)
2857
+ const TODO_COMPLETE_KNOWN_FLAGS = new Set(['--dry-run', '--raw']);
2858
+ for (const a of args.slice(3)) {
2859
+ if (typeof a === 'string' && a.startsWith('-') && !TODO_COMPLETE_KNOWN_FLAGS.has(a)) {
2860
+ error(`Unknown flag for todo complete: ${a}`, ERROR_REASON.USAGE);
2861
+ }
2862
+ }
2863
+ commands.cmdTodoComplete(cwd, args[2], { dryRun: args.includes('--dry-run') }, raw);
2575
2864
  } else if (subcommand === 'match-phase') {
2576
2865
  commands.cmdTodoMatchPhase(cwd, args[2], raw);
2577
2866
  } else {
@@ -3006,6 +3295,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3006
3295
  const RESTORE_OUTCOME = Object.freeze({
3007
3296
  ELIGIBLE: 'eligible',
3008
3297
  RESTORED: 'restored',
3298
+ ALREADY_PRESENT: 'already_present',
3009
3299
  SKIPPED_DESTINATION_MANAGED: 'skipped_destination_managed',
3010
3300
  SKIPPED_DESTINATION_EXISTS: 'skipped_destination_exists',
3011
3301
  SKIPPED_COPY_FAILED: 'skipped_copy_failed',
@@ -3064,18 +3354,30 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3064
3354
  return out;
3065
3355
  }
3066
3356
 
3067
- // Why these three checks rather than security.cjs's `validatePath`: that seam
3068
- // resolves symlinks with realpathSync and then tests containment, so a link
3069
- // whose target sits inside the config dir passes. For a restore that is still
3070
- // wrong — writing through any link overwrites whatever it points at instead
3071
- // of materializing a regular file at the backed-up path. These checks reject
3072
- // links outright, which is strictly stricter than validatePath, not a
3073
- // reimplementation of it. Do not "simplify" this to validatePath.
3357
+ // Why these three checks rather than security.cjs's `assertWithinRoot` /
3358
+ // `tryWithinRoot`: that seam resolves symlinks with realpathSync and then
3359
+ // tests containment, so a link whose target sits inside the config dir
3360
+ // passes. For a restore that is still wrong — writing through any link
3361
+ // overwrites whatever it points at instead of materializing a regular file
3362
+ // at the backed-up path. These checks reject links outright, which is
3363
+ // strictly stricter than assertWithinRoot/tryWithinRoot, not a
3364
+ // reimplementation of them. Do not "simplify" this to assertWithinRoot or
3365
+ // tryWithinRoot. Reviewed under epic #4636 Phase 3: the containment
3366
+ // DECISION now routes through the canonical lexical predicate
3367
+ // (`tryWithinRootLexical`, ADR-4650 decision 6); isInsideDir below still
3368
+ // treats target === root as NOT contained via its own extra `!==` check
3369
+ // (unlike every other containment implementation in this repo, which
3370
+ // treats target === root as contained) — that condition is this gate's
3371
+ // own and is layered on top of the shared predicate, not folded into it.
3074
3372
 
3075
3373
  /** True when `target` resolves strictly inside `root`. */
3076
3374
  function isInsideDir(root, target) {
3077
- const rel = path.relative(path.resolve(root), path.resolve(target));
3078
- return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
3375
+ // Containment decision: canonical lexical predicate (ADR-4650 decision 6).
3376
+ // The extra `!==` condition is this gate's own: a restore must never
3377
+ // target the config directory itself, only something strictly inside it.
3378
+ if (path.resolve(target) === path.resolve(root)) return false;
3379
+ const { tryWithinRootLexical } = require('./lib/security.cjs');
3380
+ return tryWithinRootLexical(target, root) !== null;
3079
3381
  }
3080
3382
 
3081
3383
  /**
@@ -3273,9 +3575,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3273
3575
  }
3274
3576
 
3275
3577
  // An identical destination is a no-op restore, not a conflict: re-running
3276
- // the restore after a successful one must stay quiet and idempotent.
3578
+ // the restore after a successful one must stay quiet and idempotent. It
3579
+ // gets its own outcome so it is excluded from eligible_count — the update
3580
+ // workflow drives its restore question off that count (#4558).
3581
+ let destExists = false;
3277
3582
  let destDiffers = false;
3278
3583
  if (fs.existsSync(destPath)) {
3584
+ destExists = true;
3279
3585
  try {
3280
3586
  destDiffers = !fs.readFileSync(destPath).equals(fs.readFileSync(srcPath));
3281
3587
  } catch {
@@ -3290,6 +3596,10 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
3290
3596
  entries.push({ path: relPath, outcome: RESTORE_OUTCOME.SKIPPED_DESTINATION_EXISTS, warnings });
3291
3597
  continue;
3292
3598
  }
3599
+ if (destExists) {
3600
+ entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ALREADY_PRESENT, warnings });
3601
+ continue;
3602
+ }
3293
3603
 
3294
3604
  if (!apply) {
3295
3605
  entries.push({ path: relPath, outcome: RESTORE_OUTCOME.ELIGIBLE, warnings });
@@ -4005,6 +4315,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
4005
4315
  * declares that file "All OS-facing I/O; single platform seam", and a private
4006
4316
  * duplicate here is what made it untrue.
4007
4317
  */
4318
+ const COMMIT_LIST_FLAGS = new Set(['--files', '--files-removed']);
4319
+
4320
+ // #4208 review: hoisted out of routeCommit's closure so the parser is reachable
4321
+ // from a test. It is the whole of the two-list argument contract, and its edge
4322
+ // cases (a boolean flag inside a run, a repeated list flag, either order) were
4323
+ // already the subject of a review round -- a parser that only the CLI can reach
4324
+ // can only be tested by example, one spawn at a time.
4325
+ //
4326
+ // Every occurrence of `flag` contributes a run; a run ends at the next LIST
4327
+ // flag and skips boolean flags on the way, so no token strictly between one
4328
+ // list flag and the next is ever dropped. Repeated runs of the same flag merge,
4329
+ // as the pre-#4208 slice-to-end parse merged them.
4330
+ function collectListFlagValues(args, flag) {
4331
+ const values = [];
4332
+ args.forEach((a, i) => {
4333
+ if (a !== flag) return;
4334
+ for (const b of args.slice(i + 1)) {
4335
+ if (COMMIT_LIST_FLAGS.has(b)) break;
4336
+ if (b.startsWith('--')) continue;
4337
+ values.push(b);
4338
+ }
4339
+ });
4340
+ return values;
4341
+ }
4342
+
4008
4343
  function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
4009
4344
  const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
4010
4345
  return resolveExecutableBinary(name, { platform, env });
@@ -4046,6 +4381,21 @@ const HOST_COMMAND_ROUTERS = {
4046
4381
  // rather than a family — ADR-2346 promotes to a family only at >=3.
4047
4382
  'estimate-check': ({ args, cwd, raw }) => estimateCli.cmdEstimateCheck(cwd, args.slice(1), raw),
4048
4383
  'estimate-calibration': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibration(cwd, args.slice(1), raw),
4384
+ // #3418: writes `last_mapped_commit` into every codebase-map document that
4385
+ // exists, closing the loop drift.cjs was built for. A LEAF verb rather than a
4386
+ // `verify` subcommand on purpose -- the verify family is read-only by
4387
+ // contract and this one mutates; ADR-2346 promotes a leaf to a family only at
4388
+ // >=3 verbs, and this is one.
4389
+ 'stamp-codebase-map': ({ args, cwd, raw, error }) => {
4390
+ const { files } = parseNamedArgsOrExit(args, { valueFlags: ['files'], positionals: 1 }, error);
4391
+ // A value flag with no value parses to `null`, same as an absent one, so
4392
+ // presence is read off `args`: a bare `--files` (an unquoted empty shell
4393
+ // variable) must hit the empty-filter refusal, not widen to all seven.
4394
+ const only = args.includes('--files')
4395
+ ? String(files ?? '').split(',').map((f) => f.trim()).filter(Boolean)
4396
+ : undefined;
4397
+ verify.cmdStampCodebaseMap(cwd, raw, only);
4398
+ },
4049
4399
  'estimate-calibrate': ({ args, cwd, raw }) => estimateCli.cmdEstimateCalibrate(cwd, args.slice(1), raw),
4050
4400
  'config-new-project': routeConfigNewProject,
4051
4401
  'config-path': routeConfigPath,
@@ -4076,9 +4426,13 @@ const HOST_COMMAND_ROUTERS = {
4076
4426
  'list-seeds': routeListSeeds,
4077
4427
  'verify-path-exists': routeVerifyPathExists,
4078
4428
  'quick-tasks-append': routeQuickTasksAppend,
4429
+ 'quick-tasks-migrate': routeQuickTasksMigrate,
4430
+ // #3676 (Phase 4, epic #3344): quick-batch coordination verbs.
4431
+ 'quick-batch': routeQuickBatchCommand,
4079
4432
  'normalize-test-command': routeNormalizeTestCommand,
4080
4433
  'dispatch-should-flatten': routeDispatchShouldFlatten,
4081
4434
  'dispatch-isolation': routeDispatchIsolation,
4435
+ 'dispatch-capacity': routeDispatchCapacity,
4082
4436
  'inspect-dispatch-isolation': routeInspectDispatchIsolation,
4083
4437
  'record-dispatch-isolation': routeRecordDispatchIsolation,
4084
4438
  'resolve-dispatch-type': routeResolveDispatchType,
@@ -4338,9 +4692,9 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
4338
4692
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
4339
4693
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
4340
4694
  'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
4341
- 'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
4342
- 'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
4343
- 'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
4695
+ 'profile-sample, progress, project-instruction-file, prompt-budget, quick-batch, quick-tasks-append, quick-tasks-migrate, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
4696
+ 'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
4697
+ 'resolve-execution, review-lane, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
4344
4698
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
4345
4699
  'Global flags:\n' +
4346
4700
  ' --raw Emit raw output without post-processing\n' +
@@ -4905,6 +5259,10 @@ module.exports = {
4905
5259
  // #3275: exported for tests — the shared PATH+PATHEXT resolver behind
4906
5260
  // review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
4907
5261
  resolveSpawnBinary,
5262
+ // #4208 review: exported for tests — the two-list commit parser is otherwise
5263
+ // reachable only by spawning the CLI, which a property test cannot afford.
5264
+ collectListFlagValues,
5265
+ COMMIT_LIST_FLAGS,
4908
5266
  // #3714 follow-up: exported for tests — the dispatch model-pin VALUE
4909
5267
  // policy (charset accept/render parity, max-length boundary, leading-char
4910
5268
  // anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.