@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -677,8 +677,25 @@ function stageValidated(opts) {
677
677
  throw new Error(`Hook fragment validation failed: ${fragErrs.join('; ')}`);
678
678
  }
679
679
  // Cross-capability validations (contract, consumes, cross-capability).
680
- const capMap = new Map([[id, cap]]);
681
- const centralKeys = new Set();
680
+ //
681
+ // #3929: seed the validation set the way the loader builds its accepted
682
+ // map — frozen first-party registry, then each committed overlay of the
683
+ // TARGET (global) install scope accepted incrementally (structural +
684
+ // engines + full-suite-clean), then the candidate LAST (mirroring
685
+ // `acceptedMap.set(id, cap)`). The singleton seed
686
+ // `new Map([[id, cap]])` this replaced made every non-empty `requires`
687
+ // unsatisfiable (membership is checked against the map) and left cycles,
688
+ // tier-monotone and central config-key exclusivity vacuous at install.
689
+ // The seed builder lives in capability-loader so the overlay semantics
690
+ // have one owner and are clean by construction — pre-existing junk in the
691
+ // install scope is skipped, never attributed to the candidate. No swallow:
692
+ // if the loader cannot build the seed the install fails loudly — silently
693
+ // degrading to the singleton map would re-hide #3929.
694
+ /* eslint-disable @typescript-eslint/no-require-imports */
695
+ const seedLoader = require('./capability-loader.cjs');
696
+ /* eslint-enable @typescript-eslint/no-require-imports */
697
+ const { capMap, centralKeys } = seedLoader.crossValidationSeed(process.cwd(), gsdHome, hostVersion, capValidator, semverMod);
698
+ capMap.set(id, cap);
682
699
  const crossErrs = [
683
700
  ...capValidator.validateAgainstContract(cap, id),
684
701
  ...capValidator.validateConsumesGlobal(capMap),
@@ -1947,7 +1947,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1947
1947
  'skillPriorityFrontmatter',
1948
1948
  'skillsGlobalOnboarding',
1949
1949
  'skillsManifestPrefix',
1950
- 'skipCodexSkillsManifest',
1951
1950
  'skipHomePrefixSubstitution',
1952
1951
  'skipSettingsUi',
1953
1952
  'skipSharedHooksInstall',
@@ -1956,6 +1955,11 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1956
1955
  'sourceMarkerFile',
1957
1956
  'tomlConfigInstall',
1958
1957
  'trackCategoryDescription',
1958
+ // #2586: declares runtime-level feature axes GSD does not/cannot support on
1959
+ // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1960
+ // — present-and-populated / absent-is-unsupported-empty convention, so
1961
+ // omitting the key on every other runtime carries no inverted meaning.
1962
+ 'unsupportedFeatures',
1959
1963
  'verificationStyle',
1960
1964
  'writeCategoryDescription',
1961
1965
  ]);
@@ -2796,7 +2800,7 @@ function materializeHookFragments(cap, capDir) {
2796
2800
 
2797
2801
  const abs = path.resolve(capDir, fragment.path);
2798
2802
  const capRoot = path.resolve(capDir);
2799
- if (abs !== capRoot && !abs.startsWith(capRoot + path.sep)) {
2803
+ if (abs !== capRoot && !abs.startsWith(capRoot + path.sep)) { // allow-handrolled-containment: committed pre-build .cjs; compiled security.cjs is untracked build output
2800
2804
  errors.push(
2801
2805
  cap.id + '/' + groupName + '[' + i + '].fragment.path escapes capability directory: ' +
2802
2806
  fragment.path,
@@ -2948,6 +2952,14 @@ function validateStep(step, prefix, declaredSkills, declaredAgents) {
2948
2952
  errors.push(prefix + '.pointFrom must be a string if present');
2949
2953
  }
2950
2954
 
2955
+ // #4209 DISP-02: strict optional boolean opt-in trait. Absent or false is
2956
+ // inert; only a literal `true` reaches the projected active hook. Reject
2957
+ // every other type (including truthy non-boolean values) so a typo can
2958
+ // never silently opt a step into reviewer-lane dispatch.
2959
+ if (step.supportsReviewerLanes !== undefined && typeof step.supportsReviewerLanes !== 'boolean') {
2960
+ errors.push(prefix + '.supportsReviewerLanes must be a boolean if present');
2961
+ }
2962
+
2951
2963
  if (step.fragment !== undefined) {
2952
2964
  errors.push(...validateFragment(step.fragment, prefix + '.fragment'));
2953
2965
  }
@@ -14,7 +14,13 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_child_process_1 = require("node:child_process");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
16
  const io = require("./io.cjs");
17
- const { output, error, ERROR_REASON } = io;
17
+ const { output, ERROR_REASON } = io;
18
+ // Explicitly annotated so TypeScript applies never-return control-flow narrowing.
19
+ // A destructured `const { error } = io` is a const WITHOUT a type annotation, and TS
20
+ // only narrows after a never-returning call when the callee is a function declaration
21
+ // or an annotated const. Without the annotation every `error(...)` guard below would
22
+ // need a dead `throw` after it to convince the checker that the value is non-null.
23
+ const error = io.error;
18
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
25
  const planningWorkspaceMod = require("./planning-workspace.cjs");
20
26
  const { planningDir } = planningWorkspaceMod;
@@ -89,7 +95,12 @@ function readIfExists(filePath) {
89
95
  }
90
96
  }
91
97
  function resolvePath(inputPath, projectDir) {
92
- return node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
98
+ const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
99
+ const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
100
+ if (contained === null) {
101
+ error(`path escapes its allowed directory: ${inputPath}`, ERROR_REASON.USAGE);
102
+ }
103
+ return contained;
93
104
  }
94
105
  function readWorkflowConfig(projectDir) {
95
106
  const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
@@ -270,11 +281,37 @@ function loadDecisionExtraction(contextPath) {
270
281
  return {
271
282
  trackable: extraction.decisions.filter((d) => d.trackable),
272
283
  outcome: extraction.outcome,
284
+ unreadableIds: extraction.unreadableIds ?? [],
273
285
  };
274
286
  }
287
+ /**
288
+ * `check decision-coverage-plan` — blocking plan-phase decision-coverage gate
289
+ * (#2492, #1365 fail-loud, #2770 empty-arg fail-closed).
290
+ *
291
+ * Invocation (the context path may be supplied EITHER way; #4130 follow-up):
292
+ * gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
293
+ * gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
294
+ *
295
+ * `--context <path>` follows the sibling flag convention (`check predicate`,
296
+ * #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
297
+ * pass, the flag WINNING over a same-purpose positional when both appear,
298
+ * and a valueless `--context` counting as no context at all (it falls
299
+ * through to the #2770 caller-error branch, not to the "CONTEXT.md missing"
300
+ * green skip). The positional form keeps working unchanged — no sibling
301
+ * check verb deprecates positionals and the plan-phase workflow passes them.
302
+ */
275
303
  function cmdDecisionCoveragePlan(projectDir, args, raw) {
276
- const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
277
- const contextArg = args[3];
304
+ // args[0]='check', args[1]=subcommand — partition the REST so flag tokens
305
+ // and their values never land in a positional slot.
306
+ const { flags, positionals } = partitionPredicateArgs(args.slice(2));
307
+ const phaseDir = positionals[0] ? resolvePath(positionals[0], projectDir) : '';
308
+ // A VALUELESS `--context` stays a bare token in the positionals (sibling
309
+ // parser semantics); it must not then be read as the context PATH — a
310
+ // `--`-prefixed "path" is a caller mistake, and #2770's law says a missing
311
+ // context argument fails CLOSED, never a silent "CONTEXT.md missing" green
312
+ // skip. So only a non-flag positional may serve as the context.
313
+ const positionalContext = positionals[1] && !positionals[1].startsWith('--') ? positionals[1] : '';
314
+ const contextArg = flags['context'] ?? positionalContext ?? '';
278
315
  const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
279
316
  if (!gateEnabled(projectDir)) {
280
317
  output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
@@ -293,28 +330,59 @@ function cmdDecisionCoveragePlan(projectDir, args, raw) {
293
330
  output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
294
331
  return;
295
332
  }
296
- const { trackable: decisions, outcome } = loadDecisionExtraction(contextPath);
333
+ // #4794: a NON-FILE path (a directory — the adjacent same-looking positional
334
+ // swapped, the issue's repro 2) is a caller error like #2770's empty argument:
335
+ // fs.existsSync is true, the read yields nothing, and the gate used to
336
+ // certify passed:true on a phase full of decisions. Fail closed, naming it.
337
+ // The stat is wrapped: a path that vanishes between existsSync and statSync
338
+ // (or any stat failure) must answer the SAME fail-closed JSON, never a throw.
339
+ let contextIsFile = false;
340
+ let contextKind = 'non-file entry';
341
+ try {
342
+ const st = node_fs_1.default.statSync(contextPath);
343
+ contextIsFile = st.isFile();
344
+ if (st.isDirectory())
345
+ contextKind = 'directory';
346
+ }
347
+ catch {
348
+ contextIsFile = false;
349
+ contextKind = 'unreadable path';
350
+ }
351
+ if (!contextIsFile) {
352
+ output({ passed: false, skipped: false, reason: 'context path is not a file', total: null, covered: null, message: `Decision coverage gate: the context path "${contextArg}" is not a readable file (${contextKind}). Swap the adjacent positionals or pass --context <path-to-CONTEXT.md>.` }, raw, undefined);
353
+ return;
354
+ }
355
+ const { trackable: decisions, outcome, unreadableIds } = loadDecisionExtraction(contextPath);
297
356
  // #1365 fail-loud gate: any could-not-parse outcome must NOT silently pass —
298
357
  // even when some decisions were extracted (e.g. D-01 valid but D-02 malformed).
299
358
  // A parse-miss on ANY bullet means the gate cannot certify full coverage.
300
359
  // Fire independent of decisions.length so a partial-parse still blocks.
301
360
  if (outcome === 'could-not-parse') {
361
+ // #4794: nothing was measured — the answer must not carry the fields of a
362
+ // gate that did. total/covered are null (a type change is the point:
363
+ // 0 reads as data, null does not), `uncovered` is OMITTED (the list was
364
+ // never built), and the ids that failed to parse are carried so a caller
365
+ // capturing stdout knows which decision to fix.
302
366
  const partialParse = decisions.length > 0;
303
367
  output({
304
368
  passed: false,
305
369
  skipped: false,
306
370
  reason: 'could-not-parse',
307
- total: decisions.length,
308
- covered: 0,
309
- uncovered: [],
310
- message: partialParse
371
+ total: null,
372
+ covered: null,
373
+ unreadable: unreadableIds,
374
+ message: (partialParse
311
375
  ? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
312
- '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator). ' +
313
- 'Fix the bullet format so all D-NN decisions can be read before re-running the gate.'
376
+ '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
377
+ 'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
378
+ 'can be read before re-running the gate.'
314
379
  : 'Decision coverage gate: could not parse decisions — possible format mismatch. ' +
315
380
  'The CONTEXT.md appears to be decision-shaped (has a <decisions> block, a decisions heading, ' +
316
- 'or D- tokens) but no D-NN bullets could be extracted. Check the formatting of the decisions ' +
317
- 'block and ensure bullets follow the `- **D-NN:** text` or `- **D-NN — title** body` form.',
381
+ 'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
382
+ 'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
383
+ 'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
384
+ 'also lands here.')
385
+ + (unreadableIds.length > 0 ? ' Unreadable ids: ' + unreadableIds.join(', ') + '.' : ''),
318
386
  }, raw, undefined);
319
387
  return;
320
388
  }
@@ -354,11 +422,6 @@ function recentCommitMessages(projectDir) {
354
422
  return '';
355
423
  }
356
424
  }
357
- function isInsideRoot(candidatePath, rootDir) {
358
- const root = node_path_1.default.resolve(rootDir);
359
- const target = node_path_1.default.resolve(root, candidatePath);
360
- return target === root || target.startsWith(`${root}${node_path_1.default.sep}`);
361
- }
362
425
  function readModifiedFilesContent(projectDir, summaries) {
363
426
  const out = [];
364
427
  let total = 0;
@@ -371,9 +434,17 @@ function readModifiedFilesContent(projectDir, summaries) {
371
434
  for (const file of files) {
372
435
  if (total >= 50)
373
436
  break;
374
- if (!file || !isInsideRoot(file, projectDir))
437
+ if (!file)
375
438
  continue;
376
- const raw = readIfExists(resolvePath(file, projectDir));
439
+ // Migrated off the hand-rolled prefix check (ADR-4650): resolve+contain in one
440
+ // step via the canonical realpath predicate — the eventual read below follows
441
+ // symlinks, so containment must be decided on the resolved target, not a lexical
442
+ // prefix. Read the value the predicate RETURNED; do not re-derive the path.
443
+ const candidate = node_path_1.default.isAbsolute(file) ? file : node_path_1.default.join(projectDir, file);
444
+ const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
445
+ if (contained === null)
446
+ continue;
447
+ const raw = readIfExists(contained);
377
448
  out.push(raw.length > 256 * 1024 ? raw.slice(0, 256 * 1024) : raw);
378
449
  total++;
379
450
  }
@@ -411,9 +482,11 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
411
482
  not_honored: [],
412
483
  message: partialParse
413
484
  ? 'Decision coverage verify (warning): decisions could not be fully parsed — one or more ' +
414
- '`- **D-NN ...**` bullets appear malformed. Fix the bullet format in the CONTEXT.md decisions block.'
485
+ '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
486
+ 'prefix that is not a digit run). Fix the bullet format in the CONTEXT.md decisions block.'
415
487
  : 'Decision coverage verify (warning): could not parse decisions — possible format mismatch. ' +
416
- 'Check the formatting of the CONTEXT.md decisions block.',
488
+ 'Check the formatting of the CONTEXT.md decisions block (accepted forms: `- **D-NN:** text`, ' +
489
+ '`- **D4-NN:** text` (phase-prefixed), `- **D-NN — title** body`).',
417
490
  }, raw, undefined);
418
491
  return;
419
492
  }
@@ -502,8 +575,10 @@ function findUiSpecInDir(phaseDir) {
502
575
  * matches the token `dashboard` exactly like the real compound `micro-frontend`
503
576
  * (the boundary rule of #3718 is intentional and untouched). The gate therefore
504
577
  * blocks only when the token match is corroborated by static frontend evidence
505
- * in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep or
506
- * a component-framework file). This mirrors the sibling post-wave gate
578
+ * in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep, a
579
+ * component-framework file, or native UI evidence — a `.xaml` file or a
580
+ * `.swift`/`.kt`/`.dart` file carrying its ecosystem's UI import marker,
581
+ * #4658). This mirrors the sibling post-wave gate
507
582
  * computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
508
583
  * matchedToken/matchedLine surface what tripped the sniffer so an operator can
509
584
  * judge the flag in one second instead of reaching for --skip-ui.
@@ -925,8 +1000,22 @@ function resolvePhaseDirOrEmpty(projectDir, phase) {
925
1000
  * phase's `-PLAN.md` files against the filesystem WITHOUT executing anything —
926
1001
  * see verify-command-grounding.cjs for the recognizer contract.
927
1002
  *
928
- * Args: check verify-command-paths <phase>
1003
+ * Args: check verify-command-paths <phase> | check verify-command-paths --dir <plan-dir>
929
1004
  * Invocable as: gsd_run check verify-command-paths <phase>
1005
+ * gsd_run check verify-command-paths --dir <plan-dir>
1006
+ *
1007
+ * `--dir` (#4767) names a directory holding `-PLAN.md` files directly, for
1008
+ * plans that live outside `.planning/phases/` — quick mode's
1009
+ * `.planning/quick/<id>/` is the motivating caller, which until #4767 never ran
1010
+ * this probe at all. The directory is resolved against the project root AND
1011
+ * CONTAINED WITHIN IT — an absolute or climbing `--dir` that lands outside the
1012
+ * root is `unresolvable`, never read — then probed exactly as a phase directory
1013
+ * is; `projectRoot` stays the project root in both forms. `--dir <value>` is the
1014
+ * only accepted spelling: `--dir=<value>` yields no `dir` flag and falls through to
1015
+ * the no-argument arm, as does an empty value. Both are `partitionPredicateArgs`
1016
+ * behaviour, inherited and unchanged. (How that parser resolves a REPEATED `--dir`
1017
+ * is deliberately not characterised here — a malformed later occurrence does not
1018
+ * displace an earlier valid one, so the obvious "last one wins" gloss is wrong.)
930
1019
  *
931
1020
  * When the phase cannot be resolved to a directory, this emits a non-throwing
932
1021
  * degraded JSON payload (status/commands/counts all zeroed, `readError`
@@ -935,18 +1024,60 @@ function resolvePhaseDirOrEmpty(projectDir, phase) {
935
1024
  * look", which a non-zero exit / thrown error would collapse.
936
1025
  */
937
1026
  function cmdVerifyCommandPaths(projectDir, args, raw) {
938
- // args[0] = 'check', args[1] = 'verify-command-paths', args[2] = phase
939
- const phase = args[2] || '';
940
- if (!phase) {
1027
+ // args[0] = 'check', args[1] = 'verify-command-paths', then either a phase
1028
+ // positional or `--dir <plan-dir>` (#4767).
1029
+ const { flags, positionals } = partitionPredicateArgs(args.slice(2));
1030
+ const dirFlag = typeof flags['dir'] === 'string' ? flags['dir'] : '';
1031
+ // First non-flag positional: `--raw` (valueless) lands in positionals too, and its position
1032
+ // relative to the phase argument is the caller's choice.
1033
+ const phase = positionals.find(p => !p.startsWith('--')) ?? '';
1034
+ if (!phase && !dirFlag) {
941
1035
  output({
942
1036
  status: 'unresolvable',
943
1037
  commands: [],
944
1038
  counts: { blocker: 0, warning: 0, total: 0 },
945
- readError: 'verify-command-paths requires a phase argument: check verify-command-paths <phase>',
1039
+ readError: 'verify-command-paths requires a phase argument or --dir: check verify-command-paths <phase> | --dir <plan-dir>',
946
1040
  }, raw, undefined);
947
1041
  return;
948
1042
  }
949
- const phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
1043
+ // `--dir` is CALLER-SUPPLIED, so it is contained before it reaches the
1044
+ // `readdirSync`/`readFileSync` calls in probePhaseVerifyCommands (#4785 review).
1045
+ // Same predicate and policy as `resolvePath` above, and for the reason ADR-4650
1046
+ // gives at the other read site: the reads below FOLLOW SYMLINKS, so containment
1047
+ // must be decided on the resolved target, not a lexical prefix — a link inside
1048
+ // the root pointing outside it passes `tryWithinRootLexical` and is then read.
1049
+ // Read the value the predicate RETURNED; never re-derive the path. An escape
1050
+ // degrades to the same non-throwing payload the unresolvable-phase arm emits,
1051
+ // because a consumer must be able to tell "could not look" from "nothing to
1052
+ // report" (and `error()` would collapse them).
1053
+ //
1054
+ // RESIDUAL, stated rather than left to be rediscovered: this is check-then-use, so
1055
+ // a symlink planted at the resolved path BETWEEN this call and the reads inside
1056
+ // probePhaseVerifyCommands would be followed. A link already in place when the
1057
+ // command runs IS refused — the predicate resolves it and returns null (driven) —
1058
+ // so the window is the in-process gap, not the ordinary case. It is a property of
1059
+ // every `tryWithinRoot` call site in this repo, including `resolvePath` above and
1060
+ // the artifact scan below, not of this arm; closing it needs O_NOFOLLOW/dirfd
1061
+ // semantics inside the ADR-4650 predicate, which is a wider change than the bug
1062
+ // this fixes.
1063
+ let phaseDir;
1064
+ if (dirFlag) {
1065
+ const candidate = node_path_1.default.isAbsolute(dirFlag) ? dirFlag : node_path_1.default.join(projectDir, dirFlag);
1066
+ const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
1067
+ if (contained === null) {
1068
+ output({
1069
+ status: 'unresolvable',
1070
+ commands: [],
1071
+ counts: { blocker: 0, warning: 0, total: 0 },
1072
+ readError: `--dir resolves outside the project root: ${dirFlag}`,
1073
+ }, raw, undefined);
1074
+ return;
1075
+ }
1076
+ phaseDir = contained;
1077
+ }
1078
+ else {
1079
+ phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
1080
+ }
950
1081
  if (!phaseDir) {
951
1082
  output({
952
1083
  status: 'unresolvable',
@@ -1019,8 +1150,9 @@ function cmdGapAnalysisPlanPost(projectDir, args, raw) {
1019
1150
  error('gap-analysis.plan-post requires a phase-dir argument: check gap-analysis.plan-post <phase-dir> [phase-req-ids]', ERROR_REASON.SDK_MISSING_ARG);
1020
1151
  return;
1021
1152
  }
1153
+ const resolvedPhaseDir = resolvePath(phaseDir, projectDir);
1022
1154
  const phaseReqIds = args[3] ?? undefined;
1023
- const result = runGapAnalysis(projectDir, phaseDir, { phaseReqIds });
1155
+ const result = runGapAnalysis(projectDir, resolvedPhaseDir, { phaseReqIds });
1024
1156
  // Uniform gate contract: block = false (gap-analysis is always advisory, never blocks).
1025
1157
  // `message` carries the human-readable gap analysis report so the dispatch's
1026
1158
  // advisory branch can surface it. --raw emits JSON (rawValue=undefined), not
@@ -1071,21 +1203,21 @@ function buildPredicateDeps() {
1071
1203
  node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
1072
1204
  return null;
1073
1205
  }
1074
- const directPath = (0, security_cjs_1.validatePath)(artifactSuffix, phaseDir);
1075
- if (directPath.safe && node_fs_1.default.existsSync(directPath.resolved) && node_fs_1.default.statSync(directPath.resolved).isFile()) {
1076
- return directPath.resolved;
1206
+ const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
1207
+ if (directContained !== null && node_fs_1.default.existsSync(directContained) && node_fs_1.default.statSync(directContained).isFile()) {
1208
+ return directContained;
1077
1209
  }
1078
- const planningPath = (0, security_cjs_1.validatePath)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
1079
- if (planningPath.safe && node_fs_1.default.existsSync(planningPath.resolved) && node_fs_1.default.statSync(planningPath.resolved).isFile()) {
1080
- return planningPath.resolved;
1210
+ const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
1211
+ if (planningContained !== null && node_fs_1.default.existsSync(planningContained) && node_fs_1.default.statSync(planningContained).isFile()) {
1212
+ return planningContained;
1081
1213
  }
1082
1214
  try {
1083
1215
  const files = node_fs_1.default.readdirSync(phaseDir);
1084
1216
  for (const f of files) {
1085
1217
  if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
1086
- const candidate = (0, security_cjs_1.validatePath)(f, phaseDir);
1087
- if (candidate.safe && node_fs_1.default.statSync(candidate.resolved).isFile())
1088
- return candidate.resolved;
1218
+ const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
1219
+ if (candidateContained !== null && node_fs_1.default.statSync(candidateContained).isFile())
1220
+ return candidateContained;
1089
1221
  }
1090
1222
  }
1091
1223
  }
@@ -1101,23 +1233,42 @@ function buildPredicateDeps() {
1101
1233
  }
1102
1234
  };
1103
1235
  }
1104
- /** Parse `--flag value` pairs from an args array into a map (last write wins). */
1105
- function parsePredicateFlags(args) {
1106
- const out = {};
1236
+ /**
1237
+ * Split an args array into `--flag value` pairs and the leftover positional
1238
+ * tokens, in ONE pass, with the semantics `check predicate` established
1239
+ * (#2008): a `--flag` followed by a non-`--` token consumes it as the value
1240
+ * (last write wins); a `--flag` with no value stays a bare token and moves to
1241
+ * the positionals; everything else is positional. `parsePredicateFlags` is
1242
+ * the flags half of this same pass — there is exactly one parser, so the
1243
+ * flag-taking check verbs cannot drift apart (#4130 follow-up: `check
1244
+ * decision-coverage-plan --context <path>` shares it).
1245
+ */
1246
+ function partitionPredicateArgs(args) {
1247
+ const flags = {};
1248
+ const positionals = [];
1107
1249
  for (let i = 0; i < args.length; i++) {
1108
1250
  const a = args[i];
1109
1251
  if (typeof a !== 'string')
1110
1252
  continue;
1111
- if (!a.startsWith('--'))
1253
+ if (!a.startsWith('--')) {
1254
+ positionals.push(a);
1112
1255
  continue;
1256
+ }
1113
1257
  const key = a.slice(2);
1114
1258
  const next = args[i + 1];
1115
1259
  if (key.length > 0 && typeof next === 'string' && !next.startsWith('--')) {
1116
- out[key] = next;
1260
+ flags[key] = next;
1117
1261
  i++;
1118
1262
  }
1263
+ else {
1264
+ positionals.push(a);
1265
+ }
1119
1266
  }
1120
- return out;
1267
+ return { flags, positionals };
1268
+ }
1269
+ /** Parse `--flag value` pairs from an args array into a map (last write wins). */
1270
+ function parsePredicateFlags(args) {
1271
+ return partitionPredicateArgs(args).flags;
1121
1272
  }
1122
1273
  /**
1123
1274
  * `check predicate` — generic evaluator for capability gate `check.predicate`
@@ -1152,10 +1303,15 @@ function cmdCheckPredicate(projectDir, args, raw) {
1152
1303
  error('predicate --predicate value must be valid JSON', ERROR_REASON.USAGE);
1153
1304
  return;
1154
1305
  }
1306
+ const rawPhaseDir = flags['phase-dir'];
1307
+ let resolvedPhaseDir = rawPhaseDir;
1308
+ if (typeof rawPhaseDir === 'string' && rawPhaseDir !== '') {
1309
+ resolvedPhaseDir = resolvePath(rawPhaseDir, projectDir);
1310
+ }
1155
1311
  const ctx = {
1156
1312
  cwd: projectDir,
1157
1313
  phaseNumber: flags['phase-number'],
1158
- phaseDir: flags['phase-dir'],
1314
+ phaseDir: resolvedPhaseDir,
1159
1315
  phaseReqIds: flags['phase-req-ids'],
1160
1316
  };
1161
1317
  let result;
@@ -1249,7 +1405,12 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
1249
1405
  // Defense-in-depth: the resolved dir must be inside the phases root (or a
1250
1406
  // milestone archive under .planning/milestones).
1251
1407
  const milestonesRoot = node_path_1.default.join(pDir, 'milestones');
1252
- if (!isInsideRoot(resolvedDir, phasesRoot) && !isInsideRoot(resolvedDir, milestonesRoot)) {
1408
+ // Lexical containment (ADR-4650): resolvedDir is a directory path, not read
1409
+ // through here — mirrors the prior path.resolve(root, candidate)-based check
1410
+ // without introducing a filesystem/realpath dependency this defense-in-depth
1411
+ // recheck never had.
1412
+ if ((0, security_cjs_1.tryWithinRootLexical)(resolvedDir, phasesRoot) === null &&
1413
+ (0, security_cjs_1.tryWithinRootLexical)(resolvedDir, milestonesRoot) === null) {
1253
1414
  output({
1254
1415
  block: true,
1255
1416
  passed: false,
@@ -1578,7 +1739,9 @@ function routeCheckCommand({ args, cwd, raw }) {
1578
1739
  // this for any gate whose `check` carries a `predicate` (instead of a `query`),
1579
1740
  // passing the predicate object as --predicate '<json>'. NOTE: unlike the
1580
1741
  // `check.query` subcommands above (which take positional phase args), this
1581
- // subcommand parses --flag value pairs.
1742
+ // subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
1743
+ // `--context <path>` (its positionals still work) — both share
1744
+ // partitionPredicateArgs, the one flag parser.
1582
1745
  cmdCheckPredicate(cwd, args, raw);
1583
1746
  return;
1584
1747
  }
@@ -1606,6 +1769,7 @@ module.exports = {
1606
1769
  cmdCheckPredicate,
1607
1770
  buildPredicateDeps,
1608
1771
  parsePredicateFlags,
1772
+ partitionPredicateArgs,
1609
1773
  // Fail-closed phase-scope reader for the api-coverage gate — exported for
1610
1774
  // in-process failure-injection tests (#2365 review).
1611
1775
  readPhaseScope,
@@ -61,7 +61,7 @@ function normalizeRelPath(p, repoRoot) {
61
61
  let relativized = false;
62
62
  if (typeof repoRoot === 'string' && repoRoot !== '') {
63
63
  const rootNormalized = repoRoot.trim().replace(/\\/g, '/').replace(/\/+$/, '');
64
- if (rootNormalized !== '' && value.startsWith(`${rootNormalized}/`)) {
64
+ if (rootNormalized !== '' && value.startsWith(`${rootNormalized}/`)) { // allow-handrolled-containment: display-path normalization — strips a caller-declared repoRoot prefix so a path renders repo-relative, not a security root-confinement decision
65
65
  value = value.slice(rootNormalized.length + 1);
66
66
  relativized = true;
67
67
  }
@@ -86,7 +86,7 @@ function normalizeRelPath(p, repoRoot) {
86
86
  * with `rulePath + '/'`. Both arguments must already be normalized. Case-sensitive.
87
87
  */
88
88
  function ruleMatchesFile(rulePath, filePath) {
89
- return filePath === rulePath || filePath.startsWith(`${rulePath}/`);
89
+ return filePath === rulePath || filePath.startsWith(`${rulePath}/`); // allow-handrolled-containment: rule-to-file segment match for selecting which review-depth rule applies — not a filesystem root-confinement gate
90
90
  }
91
91
  /**
92
92
  * Validate + normalize a single rule path (not yet matched against files).
@@ -382,9 +382,9 @@ function stripReasoningEffort(doc) {
382
382
  /**
383
383
  * The 17 roles measured as widening under derivation (declare Write/Edit,
384
384
  * never in the pre-#3897 `CODEX_AGENT_SANDBOX` map, so the old
385
- * `|| 'read-only'` fallback silently under-granted them). Pinned to
386
- * `read-only` pending the open question of whether Codex enforces
387
- * `sandbox_mode` or treats it as advisory (HALT.md). This list is CLOSED and
385
+ * `|| 'read-only'` fallback silently under-granted them) — pinned to
386
+ * `read-only` from 2026-09-08 (HALT.md) until #4770 lifted the hold on
387
+ * 2026-09-21. This list is CLOSED and
388
388
  * SHRINK-ONLY: a new writing role never lands here (S6, T26); it is validated
389
389
  * against the live tool contract every time it is consulted
390
390
  * ({@link _deriveCodexSandboxModeFromTools}) and against the real
@@ -401,29 +401,25 @@ function stripReasoningEffort(doc) {
401
401
  * parses the list correctly, so this role genuinely derives
402
402
  * `workspace-write` from its tool contract — HALT.md's original 16-role
403
403
  * count measured against the pre-fix (single-line) readers and undercounted
404
- * this role. It is held here for the same reason as the other 16: pending
405
- * Codex's `sandbox_mode` enforcement decision, not because the derivation is
406
- * wrong.
404
+ * this role.
405
+ *
406
+ * **LIFTED 2026-09-21 (#4770, maintainer decision: documented enforcement
407
+ * suffices).** The map is empty: the rung-3 hold's recorded reopen condition
408
+ * — official OpenAI documentation establishing Codex `sandbox_mode` as an
409
+ * enforced technical boundary that custom subagent TOML files honor — is
410
+ * satisfied, so every role now derives `sandbox_mode` purely from its own
411
+ * `tools:` frontmatter (`workspace-write` iff Write/Edit is declared). The
412
+ * shrink-to-zero invariant (ADR-3473 §8.3) is satisfied by reaching zero;
413
+ * the map is kept as an empty frozen structure so a future re-hold has a
414
+ * shape to land in, and {@link validateCodexSandboxHolds} keeps failing if
415
+ * the list ever grows a role that no longer exists in `agents/`. The
416
+ * #3897 security-review F1/F3 fail-closed pins are unchanged and
417
+ * map-independent: `suspicious` identities (non-ASCII after
418
+ * normalization) still pin `read-only`, and post-lift the sandbox derives
419
+ * from an artifact's own CONTENT, so the F1 identity-confusion attack no
420
+ * longer has a hold to ride.
407
421
  */
408
- exports.CODEX_SANDBOX_HOLDS = Object.freeze({
409
- 'gsd-ai-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
410
- 'gsd-code-fixer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
411
- 'gsd-code-reviewer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
412
- 'gsd-debug-session-manager': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
413
- 'gsd-doc-classifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
414
- 'gsd-doc-synthesizer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
415
- 'gsd-doc-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
416
- 'gsd-doc-writer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
417
- 'gsd-dom-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
418
- 'gsd-domain-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
419
- 'gsd-eval-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
420
- 'gsd-eval-planner': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
421
- 'gsd-intel-updater': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
422
- 'gsd-pattern-mapper': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
423
- 'gsd-ui-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
424
- 'gsd-ui-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
425
- 'gsd-nyquist-auditor': 'declares Write/Edit (YAML list-form tools:, surfaced by the list-form parse fix); pending Codex sandbox_mode enforcement decision',
426
- });
422
+ exports.CODEX_SANDBOX_HOLDS = Object.freeze({});
427
423
  // True iff a `tools:` frontmatter value declares Write or Edit as a whole
428
424
  // token (never a substring match, so a hypothetical "Edith"-named tool could
429
425
  // never collide). Single predicate owner for both the emitter