@opengsd/gsd-core 1.11.0 → 1.12.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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -0,0 +1,846 @@
1
+ "use strict";
2
+ /**
3
+ * Verify-command grounding probe (#2401).
4
+ *
5
+ * #2401: a planner authored `<automated>cd ../../frontend && npm run lint</automated>`
6
+ * whose target did not resolve from the executor's cwd, and the plan-checker —
7
+ * lacking a deterministic probe — hand-reasoned the filesystem and prescribed two
8
+ * successively-wrong replacement paths.
9
+ *
10
+ * This module answers "can this `<automated>` verify command's target directory be
11
+ * grounded?" WITHOUT ever executing the command. PLAN.md is LLM-authored untrusted
12
+ * text, so this module never `exec`s, `spawn`s, or otherwise shells out — it reads
13
+ * only via `fs.statSync`, `fs.existsSync`, `fs.readFileSync`, `fs.readdirSync`.
14
+ *
15
+ * It is a RECOGNIZER, not a shell interpreter (deliberate, per Greenspun's Tenth
16
+ * Rule — the fix for #2401 is refusing to guess, not writing a bigger shell
17
+ * parser): exactly two forms are grounded (`cd <literal>` and
18
+ * `npm --prefix <literal>`), and anything this probe cannot ground returns
19
+ * `unresolvable`, which is a warning and never a blocker.
20
+ *
21
+ * A bare ancestor climb (`cd ../..`, no trailing named segment) is genuinely
22
+ * ambiguous under parallel-worktree execution — the checker's root and the
23
+ * executor's root differ, so "my grandparent directory" cannot be asserted
24
+ * about, and it is reported `outside_root` without touching the filesystem.
25
+ * A climb that names a concrete sibling (`cd ../../frontend`, the exact #2401
26
+ * shape) still names something checkable, so it is resolved and probed like any
27
+ * other target.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_fs_1 = __importDefault(require("node:fs"));
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
36
+ const phaseIdMod = require("./phase-id.cjs");
37
+ const { stripProjectCodePrefix, extractPhaseToken, comparePhaseNum } = phaseIdMod;
38
+ // ─── Extraction ───────────────────────────────────────────────────────────────
39
+ const TASK_NAME_RE = /<name>([\s\S]*?)<\/name>/;
40
+ /**
41
+ * The body cannot cross a subsequent `<automated>` opener (negative lookahead
42
+ * `(?!<automated\b)` gates each consumed char), mirroring the stop-at-next-open
43
+ * shape `extractTaggedBlocks`/`stripTaggedBlocks` already use. This is what
44
+ * makes a scan over an unclosed opener LINEAR instead of quadratic: the lazy
45
+ * `[\s\S]*?` body used to scan all the way to EOF hunting a closing tag that
46
+ * never comes (measured: k=40000 unclosed openers, ~1.5s). `MAX_BLOCK_WALK`
47
+ * below bounds matched-block COUNT, not scan cost — it does nothing for a scan
48
+ * that matches nothing, which is exactly the case this shape fixes.
49
+ */
50
+ const AUTOMATED_BLOCK_RE = /<automated[^>]{0,200}>((?:(?!<automated\b)[\s\S])*?)<\/automated>/g;
51
+ /**
52
+ * Bounds the NUMBER of matched `<automated>` blocks walked (e.g. hundreds of
53
+ * legitimately closed blocks in one document), not the cost of scanning
54
+ * pathological unclosed input — that cost is now bounded by the non-crossing
55
+ * body in `AUTOMATED_BLOCK_RE`/`VERIFY_TOKEN_RE` themselves (a failed scan is
56
+ * linear, so no separate iteration cap is needed to keep it cheap).
57
+ * `extractTaggedBlocks`/`stripTaggedBlocks` (task-block grammar owner,
58
+ * `./markdown-sectionizer.cjs`) already use the same non-crossing shape with
59
+ * no iteration cap of their own. This guard only bounds the `<automated>`
60
+ * scan this module still runs directly, matching the pre-existing behavior.
61
+ */
62
+ const MAX_BLOCK_WALK = 20000;
63
+ /**
64
+ * Run `AUTOMATED_BLOCK_RE` over `text`, pushing `{plan: '', task, command}`
65
+ * for each non-empty trimmed block onto `out`, in order. Shares `guard`
66
+ * across every caller in one `extractAutomatedCommands` invocation so the
67
+ * MAX_BLOCK_WALK bound applies to the WHOLE document's `<automated>` count,
68
+ * not per task body. Returns `false` when the bound was hit (caller stops
69
+ * walking further task bodies immediately); `true` otherwise.
70
+ */
71
+ function extractAutomatedFromText(text, task, out, guard) {
72
+ AUTOMATED_BLOCK_RE.lastIndex = 0;
73
+ let aMatch;
74
+ while ((aMatch = AUTOMATED_BLOCK_RE.exec(text)) !== null) {
75
+ const raw = (aMatch[1] ?? '').trim();
76
+ if (raw !== '')
77
+ out.push({ plan: '', task, command: raw });
78
+ if (aMatch.index === AUTOMATED_BLOCK_RE.lastIndex)
79
+ AUTOMATED_BLOCK_RE.lastIndex += 1;
80
+ guard.n += 1;
81
+ if (guard.n > MAX_BLOCK_WALK)
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ /**
87
+ * Extract every `<automated>…</automated>` command from PLAN.md text,
88
+ * attaching the owning `<task><name>` when the block sits inside a
89
+ * `<task>…</task>`. Never throws; a non-string or blank plan yields `[]`.
90
+ * `plan` is left `''` — callers (`probePhaseVerifyCommands`,
91
+ * `harvestPriorVerifyCommands`) set it from the filename they read.
92
+ *
93
+ * Task-block bodies are obtained from the canonical sectionizer
94
+ * (`extractTaggedBlocks`/`stripTaggedBlocks`, `./markdown-sectionizer.cjs`)
95
+ * rather than a fourth hand-rolled `<task>` grammar copy (review finding,
96
+ * generative fix divergence class). This module only needs each task's
97
+ * `<name>` and its `<automated>` blocks — never the opening tag's `type=`
98
+ * attribute — so, unlike `src/verify.cts`'s `PLAN_TASK_BLOCK_RE` (which keeps
99
+ * its own copy specifically to read `type=`), it can share the owner
100
+ * outright. Ordering: every in-task command is emitted first, task by task
101
+ * in document order (`extractTaggedBlocks` returns bodies in document
102
+ * order); every command sitting OUTSIDE any `<task>` is emitted after, in
103
+ * the order it appears in the task-stripped remainder. No existing caller or
104
+ * test depends on interleaving an outside-task block between two in-task
105
+ * blocks that surround it in the raw document.
106
+ */
107
+ function extractAutomatedCommands(planText) {
108
+ if (typeof planText !== 'string' || planText.length === 0)
109
+ return [];
110
+ const out = [];
111
+ const guard = { n: 0 };
112
+ const taskBodies = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(planText, 'task', true);
113
+ for (const body of taskBodies) {
114
+ const nameMatch = TASK_NAME_RE.exec(body);
115
+ const task = nameMatch ? nameMatch[1].trim() : '';
116
+ if (!extractAutomatedFromText(body, task, out, guard))
117
+ return out;
118
+ }
119
+ const remainder = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(planText, 'task', true);
120
+ extractAutomatedFromText(remainder, '', out, guard);
121
+ return out;
122
+ }
123
+ /**
124
+ * Ordered alternation with a backreference so `<automated>` and `<fails_when>`
125
+ * are recovered in a SINGLE document-order pass — the pairing walk needs
126
+ * their relative positions, which two independent scans would discard.
127
+ *
128
+ * The body cannot cross a subsequent `<automated>` or `<fails_when>` opener
129
+ * (negative lookahead `(?!<(?:automated|fails_when)\b)`), the same
130
+ * non-crossing shape as `AUTOMATED_BLOCK_RE` above. This is what keeps a scan
131
+ * over an unclosed opener LINEAR (verified: k=10000/20000/40000 unclosed
132
+ * openers all complete in ~0ms); `MAX_BLOCK_WALK` bounds matched-block COUNT,
133
+ * not scan cost.
134
+ */
135
+ const VERIFY_TOKEN_RE = /<(automated|fails_when)[^>]{0,200}>((?:(?!<(?:automated|fails_when)\b)[\s\S])*?)<\/\1>/g;
136
+ const PLACEHOLDER_STATEMENTS = new Set(['tbd', 'todo', 'n/a', 'na', 'none', 'unknown', 'tba', '?', '-']);
137
+ /**
138
+ * Judge one `<automated>` command's stated failing direction. Pure, total,
139
+ * and never throws for any input shape — order of checks is load-bearing
140
+ * (#3172 review): the sentinel exemption (step 3) must run BEFORE the
141
+ * statement-presence check (step 4), because the Nyquist Wave-0 sentinel is
142
+ * not a runnable command and so has no failure mode to state, even when a
143
+ * statement happens to follow it in the plan text.
144
+ */
145
+ function resolveFailingDirection(command, statement) {
146
+ const cmd = typeof command === 'string' ? command.trim() : '';
147
+ if (cmd === '') {
148
+ return {
149
+ command: '',
150
+ statement: typeof statement === 'string' ? statement : null,
151
+ status: 'orphan',
152
+ severity: 'warning',
153
+ };
154
+ }
155
+ if (MISSING_SENTINEL_RE.test(cmd)) {
156
+ return {
157
+ command: cmd,
158
+ statement: typeof statement === 'string' ? statement.trim() : null,
159
+ status: 'sentinel',
160
+ severity: 'none',
161
+ };
162
+ }
163
+ if (typeof statement !== 'string') {
164
+ return { command: cmd, statement: null, status: 'missing', severity: 'blocker' };
165
+ }
166
+ const trimmed = statement.trim();
167
+ if (trimmed === '') {
168
+ return { command: cmd, statement, status: 'empty', severity: 'blocker' };
169
+ }
170
+ // Whole-value match only — never a substring test. "TBD in the harness
171
+ // output" is real prose and must pass (#3172 review Finding).
172
+ if (PLACEHOLDER_STATEMENTS.has(trimmed.toLowerCase())) {
173
+ return { command: cmd, statement: trimmed, status: 'placeholder', severity: 'blocker' };
174
+ }
175
+ return { command: cmd, statement: trimmed, status: 'ok', severity: 'none' };
176
+ }
177
+ /**
178
+ * Scan one text unit's `<automated>`/`<fails_when>` tokens in document order,
179
+ * binding each `<fails_when>` to the nearest PRECEDING `<automated>` (first
180
+ * statement wins; a redundant trailing statement adds no row) and emitting an
181
+ * `orphan` row for a `<fails_when>` with no pending command. Shares `guard`
182
+ * across every text unit in one `extractFailingDirections` call, mirroring
183
+ * `extractAutomatedFromText`'s MAX_BLOCK_WALK bound and zero-length-match
184
+ * advance. Returns `false` when the bound was hit; `true` otherwise.
185
+ */
186
+ function extractFailingFromText(text, task, out, guard) {
187
+ VERIFY_TOKEN_RE.lastIndex = 0;
188
+ let pending = null;
189
+ let bound = false;
190
+ let match;
191
+ while ((match = VERIFY_TOKEN_RE.exec(text)) !== null) {
192
+ const kind = match[1];
193
+ const body = match[2] ?? '';
194
+ if (kind === 'automated') {
195
+ const trimmedCommand = body.trim();
196
+ if (trimmedCommand !== '') {
197
+ const entry = { ...resolveFailingDirection(trimmedCommand, undefined), plan: '', task };
198
+ out.push(entry);
199
+ pending = entry;
200
+ bound = false;
201
+ }
202
+ }
203
+ else {
204
+ // kind === 'fails_when'
205
+ if (pending === null) {
206
+ const entry = { ...resolveFailingDirection('', body), plan: '', task };
207
+ out.push(entry);
208
+ }
209
+ else if (!bound) {
210
+ const resolved = resolveFailingDirection(pending.command, body);
211
+ pending.statement = resolved.statement;
212
+ pending.status = resolved.status;
213
+ pending.severity = resolved.severity;
214
+ bound = true;
215
+ }
216
+ // already bound → redundant trailing statement, ignored (first-wins).
217
+ }
218
+ if (match.index === VERIFY_TOKEN_RE.lastIndex)
219
+ VERIFY_TOKEN_RE.lastIndex += 1;
220
+ guard.n += 1;
221
+ if (guard.n > MAX_BLOCK_WALK)
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Extract every `<automated>`/`<fails_when>` pairing verdict from PLAN.md
228
+ * text. Never throws; a non-string or blank plan yields `[]`. `plan` is left
229
+ * `''`, mirroring `extractAutomatedCommands` — callers set it from the
230
+ * filename they read.
231
+ *
232
+ * Walks the SAME text units, in the SAME order, as `extractAutomatedCommands`
233
+ * (every `<task>` body first in document order, then the task-stripped
234
+ * remainder as a final unit with task `''`) so pairing never crosses a task
235
+ * boundary, and shares ONE `{n:0}` guard across every unit so
236
+ * `MAX_BLOCK_WALK` bounds the whole document.
237
+ */
238
+ function extractFailingDirections(planText) {
239
+ if (typeof planText !== 'string' || planText.length === 0)
240
+ return [];
241
+ const out = [];
242
+ const guard = { n: 0 };
243
+ const taskBodies = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(planText, 'task', true);
244
+ for (const body of taskBodies) {
245
+ const nameMatch = TASK_NAME_RE.exec(body);
246
+ const task = nameMatch ? nameMatch[1].trim() : '';
247
+ if (!extractFailingFromText(body, task, out, guard))
248
+ return out;
249
+ }
250
+ const remainder = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(planText, 'task', true);
251
+ extractFailingFromText(remainder, '', out, guard);
252
+ return out;
253
+ }
254
+ /**
255
+ * Probe every `<automated>` command's stated failing direction across a
256
+ * phase directory's `-PLAN.md` files. Never throws — a read failure degrades
257
+ * to `readError` with the offending file skipped, and an empty result with a
258
+ * populated `readError` means "could not look", which must never render as
259
+ * "nothing to report".
260
+ */
261
+ function probePhaseFailingDirections(options) {
262
+ const { phaseDir } = options;
263
+ let entries;
264
+ try {
265
+ entries = node_fs_1.default.readdirSync(phaseDir);
266
+ }
267
+ catch (err) {
268
+ return {
269
+ status: 'unresolvable',
270
+ commands: [],
271
+ counts: { blocker: 0, warning: 0, total: 0 },
272
+ readError: toMessage(err),
273
+ };
274
+ }
275
+ const planFiles = entries.filter(f => PLAN_FILE_RE.test(f)).sort();
276
+ const commands = [];
277
+ const readErrors = [];
278
+ for (const file of planFiles) {
279
+ let text;
280
+ try {
281
+ text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, file), 'utf-8');
282
+ }
283
+ catch (err) {
284
+ readErrors.push(toMessage(err));
285
+ continue;
286
+ }
287
+ for (const entry of extractFailingDirections(text)) {
288
+ commands.push({ ...entry, plan: file });
289
+ }
290
+ }
291
+ const counts = { blocker: 0, warning: 0, total: commands.length };
292
+ for (const c of commands) {
293
+ if (c.severity === 'blocker')
294
+ counts.blocker += 1;
295
+ else if (c.severity === 'warning')
296
+ counts.warning += 1;
297
+ }
298
+ const readError = readErrors.length > 0 ? readErrors.join('; ') : null;
299
+ // A populated readError can never yield 'ok' — "could not look" must never
300
+ // render as "nothing to report" (#3172 review self-finding: with the old
301
+ // `commands.length === 0 && readError !== null` precedence, a phase where
302
+ // one plan read fine with zero blockers and a SECOND plan failed to read
303
+ // fell through to 'ok' because commands.length > 0). 'blocked' keeps
304
+ // precedence over 'unresolvable' because a proven blocker is the stronger
305
+ // signal.
306
+ let status;
307
+ if (counts.blocker > 0) {
308
+ status = 'blocked';
309
+ }
310
+ else if (readError !== null) {
311
+ status = 'unresolvable';
312
+ }
313
+ else {
314
+ status = 'ok';
315
+ }
316
+ return { status, commands, counts, readError };
317
+ }
318
+ // ─── Resolution ───────────────────────────────────────────────────────────────
319
+ /**
320
+ * `$`, backtick, `*`, `?`, or a newline — a path this recognizer refuses to
321
+ * guess at anywhere in the string. `$`/backtick are substitution, `*`/`?` are
322
+ * shell globs (and illegal in Windows path components regardless), and a
323
+ * newline is never a valid single path.
324
+ */
325
+ const DYNAMIC_PATH_ANYWHERE_RE = /[$`*?\n]/;
326
+ /**
327
+ * A LEADING `~` is shell home-expansion (`~/web`, `~user/web`) and is refused
328
+ * as dynamic; the dynamic check runs BEFORE quote stripping (so `cd
329
+ * "$FRONTEND"` is still caught), so this tolerates one leading quote char
330
+ * before the `~`. A `~` anywhere else in a path is an ordinary literal
331
+ * character — e.g. Windows 8.3 short names like `RUNNER~1` — and must resolve
332
+ * normally rather than being refused (#2401 CI regression).
333
+ */
334
+ const DYNAMIC_PATH_LEADING_TILDE_RE = /^["']?~/;
335
+ const CD_SEGMENT_RE = /^cd\s+(.+)$/;
336
+ /**
337
+ * Quote-aware `--prefix` value capture (#2401 review Finding 2): a bare
338
+ * `\S+` capture truncates a quoted path containing a space (`--prefix "my
339
+ * dir"` → `"my`). Alternation order is double-quoted, single-quoted,
340
+ * unquoted — the surrounding quote pair (if any) is removed downstream by
341
+ * the existing `stripQuotes`.
342
+ */
343
+ const PREFIX_FLAG_RE = /(?:^|\s)--prefix(?:=|\s+)("[^"]*"|'[^']*'|\S+)/;
344
+ /** Same value grammar as `PREFIX_FLAG_RE`, `g`-flagged for stripping (Finding 1). */
345
+ const PREFIX_FLAG_STRIP_RE = /(?:^|\s)--prefix(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g;
346
+ const NEEDS_NPM_RE = /^(npm|npx|pnpm|yarn|bun)\b/;
347
+ const NEEDS_MAKE_RE = /^make\b/;
348
+ const NPM_RUN_SCRIPT_RE = /\bnpm\s+run\s+([\w:@./-]+)/;
349
+ /**
350
+ * The `\s|$` anchor (not `\b`) is load-bearing: `\b` also matches an env-var
351
+ * assignment prefix such as `MISSING=1 cmd` (`=` is a non-word char, so `\b`
352
+ * sits happily before it) — and that IS a real runnable command, not the
353
+ * Nyquist Wave-0 sentinel. #3172 wires this constant into a BLOCKING gate
354
+ * (`resolveFailingDirection`), so a benign #2401 false-positive on the `\b`
355
+ * form becomes a real gate bypass: `MISSING=true npm test` would be reported
356
+ * `status:'sentinel', severity:'none'` instead of `missing`/`blocker`
357
+ * (#3172 review).
358
+ */
359
+ const MISSING_SENTINEL_RE = /^MISSING(?:\s|$)/;
360
+ const MAX_MANIFEST_BYTES = 512 * 1024;
361
+ function emptyResult(base) {
362
+ return {
363
+ command: '',
364
+ status: 'not_applicable',
365
+ severity: 'none',
366
+ reason: null,
367
+ form: null,
368
+ rawTarget: null,
369
+ target: null,
370
+ manifest: null,
371
+ script: null,
372
+ sentinel: false,
373
+ base,
374
+ };
375
+ }
376
+ /** Split on `&&`, `||`, `;`, and newlines; trim each segment; drop empties. No quote-awareness. */
377
+ function splitSegments(cmd) {
378
+ return cmd
379
+ .split(/&&|\|\||;|\n/)
380
+ .map(s => s.trim())
381
+ .filter(s => s.length > 0);
382
+ }
383
+ /** Strip a single matching pair of surrounding quotes, if present. */
384
+ function stripQuotes(s) {
385
+ if (s.length >= 2) {
386
+ const first = s[0];
387
+ const last = s[s.length - 1];
388
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
389
+ return s.slice(1, -1);
390
+ }
391
+ }
392
+ return s;
393
+ }
394
+ /** Backslash → forward-slash, applied unconditionally (backslash paths arrive on Linux too). */
395
+ function toSlash(s) {
396
+ return s.replace(/\\/g, '/');
397
+ }
398
+ /**
399
+ * `NPM_RUN_SCRIPT_RE` requires `npm` and `run` adjacent, so `npm --prefix
400
+ * ./web run lint` (the form this feature's own docs prefer) never matches
401
+ * (#2401 review Finding 1). Strip the `--prefix <value>` / `--prefix=<value>`
402
+ * flag (either ordering, quote-aware) before running the script-name match.
403
+ */
404
+ function stripPrefixFlag(s) {
405
+ return s.replace(PREFIX_FLAG_STRIP_RE, ' ');
406
+ }
407
+ /** Is `raw` (after the same quote-stripping/slash-normalization used elsewhere) an absolute path? */
408
+ function isAbsoluteCdSegment(raw) {
409
+ return node_path_1.default.isAbsolute(toSlash(stripQuotes(raw)));
410
+ }
411
+ /**
412
+ * Fold chained `cd` segments left-to-right with absolute-reset semantics
413
+ * (#2401 review Finding 3): a relative segment appends onto the
414
+ * accumulator; an absolute segment discards everything accumulated so far
415
+ * and becomes the new accumulator (matching real shell `cd` semantics —
416
+ * `cd sub && cd /abs/path` ends up at `/abs/path`, not `sub//abs/path`).
417
+ * A single segment (the overwhelmingly common case) always returns that
418
+ * segment verbatim, byte-identical to the pre-fix `cdArgs[0]` behavior.
419
+ */
420
+ function foldCdArgs(args) {
421
+ let acc = '';
422
+ for (const raw of args) {
423
+ if (acc === '' || isAbsoluteCdSegment(raw)) {
424
+ acc = raw;
425
+ }
426
+ else {
427
+ acc = `${acc}/${raw}`;
428
+ }
429
+ }
430
+ return acc;
431
+ }
432
+ function stripLeadingDotSlash(s) {
433
+ return s.replace(/^\.\//, '');
434
+ }
435
+ /**
436
+ * A relative escape whose every segment is `..` (a bare ancestor climb, e.g.
437
+ * `cd ../..`) is inherently ambiguous across worktrees — flagged `outside_root`
438
+ * without touching the filesystem. A climb that names a concrete sibling (e.g.
439
+ * `cd ../../frontend`, the exact #2401 shape) still names something checkable
440
+ * and falls through to the normal filesystem probe below.
441
+ */
442
+ function isPureAncestorClimb(rel) {
443
+ if (rel.length === 0)
444
+ return false;
445
+ const segs = rel.split(/[\\/]/).filter(s => s.length > 0);
446
+ return segs.length > 0 && segs.every(s => s === '..');
447
+ }
448
+ function declaredPathCovers(declaredPaths, norm) {
449
+ if (!Array.isArray(declaredPaths))
450
+ return false;
451
+ const target = stripLeadingDotSlash(norm);
452
+ return declaredPaths.some(p => {
453
+ if (typeof p !== 'string')
454
+ return false;
455
+ const dp = stripLeadingDotSlash(toSlash(p));
456
+ return dp === target || dp.startsWith(target + '/');
457
+ });
458
+ }
459
+ /**
460
+ * Read a package.json manifest bounded by size and guarded end-to-end; never
461
+ * throws. Returns `null` when the manifest cannot be read/parsed/shaped, or
462
+ * the parsed value when it is usable.
463
+ */
464
+ function readManifestObject(manifestPath) {
465
+ let size = 0;
466
+ try {
467
+ size = node_fs_1.default.statSync(manifestPath).size;
468
+ }
469
+ catch {
470
+ return null;
471
+ }
472
+ if (size > MAX_MANIFEST_BYTES)
473
+ return null;
474
+ let raw;
475
+ try {
476
+ raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
477
+ }
478
+ catch {
479
+ return null;
480
+ }
481
+ let parsed;
482
+ try {
483
+ parsed = JSON.parse(raw);
484
+ }
485
+ catch {
486
+ return null;
487
+ }
488
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
489
+ return null;
490
+ return parsed;
491
+ }
492
+ /**
493
+ * Resolve a single `<automated>` command's target directory WITHOUT ever
494
+ * executing it. Every `fs` call is individually guarded, so this function
495
+ * never throws for any input — including a bare call with no options.
496
+ */
497
+ function resolveVerifyCommandTarget(command, options) {
498
+ const opts = options && typeof options === 'object' ? options : {};
499
+ const base = typeof opts.projectRoot === 'string' && opts.projectRoot !== '' ? opts.projectRoot : process.cwd();
500
+ const result = emptyResult(base);
501
+ if (typeof command !== 'string')
502
+ return result;
503
+ result.command = command;
504
+ const trimmed = command.trim();
505
+ if (trimmed === '')
506
+ return result;
507
+ // Nyquist "MISSING — Wave 0 must create …" sentinel; Dimension 8 owns it.
508
+ if (MISSING_SENTINEL_RE.test(trimmed)) {
509
+ result.sentinel = true;
510
+ return result;
511
+ }
512
+ const segments = splitSegments(trimmed);
513
+ let form = null;
514
+ let rawTarget = null;
515
+ let rest;
516
+ const cdArgs = [];
517
+ let i = 0;
518
+ while (i < segments.length) {
519
+ const m = CD_SEGMENT_RE.exec(segments[i]);
520
+ if (!m)
521
+ break;
522
+ cdArgs.push(m[1].trim());
523
+ i += 1;
524
+ }
525
+ if (cdArgs.length > 0) {
526
+ form = 'cd';
527
+ rawTarget = foldCdArgs(cdArgs);
528
+ rest = segments.slice(i).join(' && ');
529
+ }
530
+ else {
531
+ const prefixMatch = PREFIX_FLAG_RE.exec(trimmed);
532
+ if (!prefixMatch)
533
+ return result; // not_applicable — neither form matched
534
+ form = 'prefix';
535
+ rawTarget = prefixMatch[1];
536
+ rest = trimmed;
537
+ }
538
+ // Dynamic-path refusal, tested against rawTarget BEFORE any unquoting.
539
+ if (DYNAMIC_PATH_ANYWHERE_RE.test(rawTarget) || DYNAMIC_PATH_LEADING_TILDE_RE.test(rawTarget)) {
540
+ result.status = 'unresolvable';
541
+ result.reason = 'dynamic_path';
542
+ result.severity = 'warning';
543
+ result.form = form;
544
+ result.rawTarget = rawTarget;
545
+ return result;
546
+ }
547
+ const norm = toSlash(stripQuotes(rawTarget));
548
+ const isAbs = node_path_1.default.isAbsolute(norm);
549
+ const target = isAbs ? node_path_1.default.normalize(norm) : node_path_1.default.resolve(base, norm);
550
+ result.form = form;
551
+ result.rawTarget = rawTarget;
552
+ result.target = target;
553
+ if (!isAbs) {
554
+ const rel = node_path_1.default.relative(base, target);
555
+ if (isPureAncestorClimb(rel)) {
556
+ result.status = 'ok';
557
+ result.severity = 'warning';
558
+ result.reason = 'outside_root';
559
+ return result;
560
+ }
561
+ }
562
+ let stat;
563
+ try {
564
+ stat = node_fs_1.default.statSync(target, { throwIfNoEntry: false });
565
+ }
566
+ catch {
567
+ stat = undefined;
568
+ }
569
+ if (!stat || !stat.isDirectory()) {
570
+ if (declaredPathCovers(opts.declaredPaths, norm)) {
571
+ result.status = 'pending_creation';
572
+ result.severity = 'none';
573
+ result.reason = null;
574
+ }
575
+ else {
576
+ result.status = 'broken';
577
+ result.reason = 'missing_dir';
578
+ result.severity = 'blocker';
579
+ }
580
+ return result;
581
+ }
582
+ const restTrimmed = rest.trim();
583
+ let neededManifest = null;
584
+ if (NEEDS_NPM_RE.test(restTrimmed))
585
+ neededManifest = 'package.json';
586
+ else if (NEEDS_MAKE_RE.test(restTrimmed))
587
+ neededManifest = 'Makefile';
588
+ if (!neededManifest) {
589
+ result.status = 'ok';
590
+ result.severity = 'none';
591
+ return result;
592
+ }
593
+ const manifestPath = node_path_1.default.join(target, neededManifest);
594
+ let manifestExists = false;
595
+ try {
596
+ manifestExists = node_fs_1.default.existsSync(manifestPath);
597
+ }
598
+ catch {
599
+ manifestExists = false;
600
+ }
601
+ if (!manifestExists) {
602
+ result.status = 'broken';
603
+ result.reason = 'no_manifest';
604
+ result.severity = 'blocker';
605
+ return result;
606
+ }
607
+ result.manifest = neededManifest;
608
+ if (neededManifest === 'Makefile') {
609
+ result.status = 'ok';
610
+ result.severity = 'none';
611
+ return result;
612
+ }
613
+ const parsed = readManifestObject(manifestPath);
614
+ if (!parsed) {
615
+ result.status = 'ok';
616
+ result.reason = 'manifest_unreadable';
617
+ result.severity = 'warning';
618
+ return result;
619
+ }
620
+ const scriptMatch = NPM_RUN_SCRIPT_RE.exec(stripPrefixFlag(restTrimmed));
621
+ if (scriptMatch) {
622
+ const scriptName = scriptMatch[1];
623
+ result.script = scriptName;
624
+ const scripts = parsed['scripts'];
625
+ const hasScript = scripts !== null &&
626
+ typeof scripts === 'object' &&
627
+ !Array.isArray(scripts) &&
628
+ Object.prototype.hasOwnProperty.call(scripts, scriptName);
629
+ if (!hasScript) {
630
+ result.status = 'ok';
631
+ result.reason = 'script_missing';
632
+ result.severity = 'warning';
633
+ return result;
634
+ }
635
+ }
636
+ result.status = 'ok';
637
+ result.severity = 'none';
638
+ return result;
639
+ }
640
+ // ─── Phase probing ────────────────────────────────────────────────────────────
641
+ const PLAN_FILE_RE = /-PLAN\.md$/i;
642
+ const ARTIFACTS_HEADING_RE = /^##[ \t]+Artifacts this phase produces\s*$/m;
643
+ const NEXT_HEADING_RE = /^##[ \t]+/m;
644
+ const FILES_BLOCK_RE = /<files>([\s\S]*?)<\/files>/g;
645
+ const ARTIFACT_BULLET_RE = /^-[ \t]+(.+)$/gm;
646
+ function toMessage(err) {
647
+ return err instanceof Error ? err.message : String(err);
648
+ }
649
+ /**
650
+ * Every `<files>…</files>` body (split on commas/whitespace/newlines) plus
651
+ * every `- ` bullet under a `## Artifacts this phase produces` heading (up to
652
+ * the next `## ` heading), scanned across the WHOLE plan text.
653
+ */
654
+ function extractDeclaredPaths(text) {
655
+ const out = [];
656
+ FILES_BLOCK_RE.lastIndex = 0;
657
+ let fMatch;
658
+ while ((fMatch = FILES_BLOCK_RE.exec(text)) !== null) {
659
+ const body = fMatch[1] ?? '';
660
+ for (const tok of body.split(/[,\s]+/)) {
661
+ const t = tok.trim();
662
+ if (t)
663
+ out.push(t);
664
+ }
665
+ }
666
+ const headingMatch = ARTIFACTS_HEADING_RE.exec(text);
667
+ if (headingMatch) {
668
+ const after = text.slice(headingMatch.index + headingMatch[0].length);
669
+ const nextIdx = after.search(NEXT_HEADING_RE);
670
+ const section = nextIdx === -1 ? after : after.slice(0, nextIdx);
671
+ ARTIFACT_BULLET_RE.lastIndex = 0;
672
+ let bMatch;
673
+ while ((bMatch = ARTIFACT_BULLET_RE.exec(section)) !== null) {
674
+ const t = (bMatch[1] ?? '').trim();
675
+ if (t)
676
+ out.push(t);
677
+ }
678
+ }
679
+ return out;
680
+ }
681
+ const STATUS_PRIORITY = ['broken', 'unresolvable', 'pending_creation', 'ok', 'not_applicable'];
682
+ /**
683
+ * Probe every `<automated>` command in a phase directory's `-PLAN.md` files
684
+ * against the filesystem. Never throws — a read failure degrades to
685
+ * `readError` with the offending file skipped.
686
+ */
687
+ function probePhaseVerifyCommands(options) {
688
+ const { phaseDir, projectRoot } = options;
689
+ let entries;
690
+ try {
691
+ entries = node_fs_1.default.readdirSync(phaseDir);
692
+ }
693
+ catch (err) {
694
+ return {
695
+ status: 'unresolvable',
696
+ commands: [],
697
+ counts: { blocker: 0, warning: 0, total: 0 },
698
+ readError: toMessage(err),
699
+ };
700
+ }
701
+ const planFiles = entries.filter(f => PLAN_FILE_RE.test(f)).sort();
702
+ const commands = [];
703
+ const readErrors = [];
704
+ for (const file of planFiles) {
705
+ let text;
706
+ try {
707
+ text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, file), 'utf-8');
708
+ }
709
+ catch (err) {
710
+ readErrors.push(toMessage(err));
711
+ continue;
712
+ }
713
+ const declaredPaths = extractDeclaredPaths(text);
714
+ const extracted = extractAutomatedCommands(text);
715
+ for (const cmd of extracted) {
716
+ const resolved = resolveVerifyCommandTarget(cmd.command, { projectRoot, declaredPaths });
717
+ commands.push({ ...resolved, plan: file, task: cmd.task });
718
+ }
719
+ }
720
+ const counts = { blocker: 0, warning: 0, total: commands.length };
721
+ for (const c of commands) {
722
+ if (c.severity === 'blocker')
723
+ counts.blocker += 1;
724
+ else if (c.severity === 'warning')
725
+ counts.warning += 1;
726
+ }
727
+ let status = 'ok';
728
+ if (commands.length > 0) {
729
+ const present = new Set(commands.map(c => c.status));
730
+ status = STATUS_PRIORITY.find(s => present.has(s)) ?? 'ok';
731
+ }
732
+ return {
733
+ status,
734
+ commands,
735
+ counts,
736
+ readError: readErrors.length > 0 ? readErrors.join('; ') : null,
737
+ };
738
+ }
739
+ // ─── Prior-phase harvesting ───────────────────────────────────────────────────
740
+ const DEFAULT_LIMIT = 20;
741
+ const DEFAULT_LOOKBACK = 3;
742
+ /**
743
+ * Walk backward from `beforePhase` (descending, at most `lookback` phase
744
+ * directories) looking for the nearest prior phase whose plans carry any
745
+ * `<automated>` command. Returns that phase's commands, deduped by command
746
+ * text (first-seen order) and capped at `limit`. Never throws.
747
+ *
748
+ * Phase directories are enumerated and ordered via the canonical grammar in
749
+ * `phase-id.cjs` (#2401 review fix) rather than a bespoke `phase-N-slug`
750
+ * regex — real GSD phase directories are `01-foundation`, `3-thing`,
751
+ * `2.1-thing`, `12A-thing`, or project-code-prefixed (`CK-01-name`), never
752
+ * `phase-N-slug`. A directory name is treated as a phase directory only when
753
+ * it (after stripping an optional project-code prefix) starts with a digit;
754
+ * `notes`, `archive`, etc. are ignored. `extractPhaseToken` reads each
755
+ * directory's phase token and `comparePhaseNum` both filters (`< beforePhase`)
756
+ * and orders (descending) so decimal/lettered/sentinel tokens (`2.1`, `12A`,
757
+ * `999.1`) compare correctly instead of via lossy `Number()` parsing.
758
+ */
759
+ function harvestPriorVerifyCommands(options) {
760
+ const { planningDir, beforePhase, limit = DEFAULT_LIMIT, lookback = DEFAULT_LOOKBACK } = options;
761
+ let entries;
762
+ try {
763
+ entries = node_fs_1.default.readdirSync(planningDir, { withFileTypes: true });
764
+ }
765
+ catch (err) {
766
+ return { commands: [], readError: toMessage(err) };
767
+ }
768
+ const beforePhaseStr = String(beforePhase);
769
+ const candidates = [];
770
+ for (const ent of entries) {
771
+ let isDir = false;
772
+ try {
773
+ isDir = ent.isDirectory();
774
+ }
775
+ catch {
776
+ isDir = false;
777
+ }
778
+ if (!isDir)
779
+ continue;
780
+ // Not a phase directory at all (e.g. 'notes', 'archive') — no reliable
781
+ // phase token can be read from a name that doesn't start with a digit
782
+ // once any project-code prefix ('CK-', 'PROJ-') is stripped.
783
+ if (!/^\d/.test(stripProjectCodePrefix(ent.name)))
784
+ continue;
785
+ const token = extractPhaseToken(ent.name);
786
+ if (comparePhaseNum(token, beforePhaseStr) < 0) {
787
+ candidates.push({ token, dir: ent.name });
788
+ }
789
+ }
790
+ candidates.sort((a, b) => comparePhaseNum(b.token, a.token));
791
+ const readErrors = [];
792
+ let examined = 0;
793
+ for (const candidate of candidates) {
794
+ if (examined >= lookback)
795
+ break;
796
+ examined += 1;
797
+ const phaseDirPath = node_path_1.default.join(planningDir, candidate.dir);
798
+ let planFiles;
799
+ try {
800
+ planFiles = node_fs_1.default.readdirSync(phaseDirPath).filter(f => PLAN_FILE_RE.test(f)).sort();
801
+ }
802
+ catch (err) {
803
+ readErrors.push(toMessage(err));
804
+ continue;
805
+ }
806
+ const seen = new Set();
807
+ const found = [];
808
+ for (const file of planFiles) {
809
+ let text;
810
+ try {
811
+ text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDirPath, file), 'utf-8');
812
+ }
813
+ catch (err) {
814
+ readErrors.push(toMessage(err));
815
+ continue;
816
+ }
817
+ for (const cmd of extractAutomatedCommands(text)) {
818
+ if (seen.has(cmd.command))
819
+ continue;
820
+ seen.add(cmd.command);
821
+ found.push({
822
+ phase: candidate.token,
823
+ plan: node_path_1.default.join(phaseDirPath, file),
824
+ task: cmd.task,
825
+ command: cmd.command,
826
+ });
827
+ }
828
+ }
829
+ if (found.length > 0) {
830
+ return {
831
+ commands: found.slice(0, limit),
832
+ readError: readErrors.length > 0 ? readErrors.join('; ') : null,
833
+ };
834
+ }
835
+ }
836
+ return { commands: [], readError: readErrors.length > 0 ? readErrors.join('; ') : null };
837
+ }
838
+ module.exports = {
839
+ extractAutomatedCommands,
840
+ resolveVerifyCommandTarget,
841
+ probePhaseVerifyCommands,
842
+ harvestPriorVerifyCommands,
843
+ resolveFailingDirection,
844
+ extractFailingDirections,
845
+ probePhaseFailingDirections,
846
+ };