@opengsd/gsd-core 1.14.0 → 1.16.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 (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -22,27 +22,18 @@ const frontmatterMod = require("./frontmatter.cjs");
22
22
  const stateMod = require("./state.cjs");
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-profiles.cjs is an export= CommonJS module
24
24
  const modelProfilesMod = require("./model-profiles.cjs");
25
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
26
- const planScanMod = require("./plan-scan.cjs");
27
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
28
- const verificationMod = require("./verification.cjs");
29
25
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
30
26
  const coreUtilsMod = require("./core-utils.cjs");
31
27
  const { findOrphanSummaries, findUnsummarizedPlans } = coreUtilsMod;
32
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
33
- const planningScopeMod = require("./planning-scope.cjs");
34
- const { SCOPE } = planningScopeMod;
35
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
36
- const worktreeSafetyMod = require("./worktree-safety.cjs");
37
- // Single owner of git C-quoted-path decoding (see #4081 note at the
38
- // codebase-drift --name-status parse loop).
39
- const { decodeGitQuotedPath } = worktreeSafetyMod;
40
28
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
41
29
  const security_cjs_2 = require("./security.cjs");
42
30
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
43
31
  const schema_detect_cjs_1 = require("./schema-detect.cjs");
44
32
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
45
33
  const pattern_cjs_1 = require("./pattern.cjs");
34
+ const gate_exit_cjs_1 = require("./gate-exit.cjs");
35
+ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
36
+ const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
46
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
47
38
  const agentInstallCheck = require("./agent-install-check.cjs");
48
39
  const { checkAgentsInstalled, checkCodexModelPosture, checkCodexSandboxPosture } = agentInstallCheck;
@@ -50,9 +41,6 @@ const { checkAgentsInstalled, checkCodexModelPosture, checkCodexSandboxPosture }
50
41
  const ioMod = require("./io.cjs");
51
42
  const { output, error } = ioMod;
52
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports
53
- const phaseIdMod = require("./phase-id.cjs");
54
- const { normalizePhaseName, matchPhaseDirs } = phaseIdMod;
55
- // eslint-disable-next-line @typescript-eslint/no-require-imports
56
44
  const phaseLocatorMod = require("./phase-locator.cjs");
57
45
  const { findPhaseInternal } = phaseLocatorMod;
58
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -68,8 +56,7 @@ const { buildPlanningSnapshot } = planningSnapshotMod;
68
56
  const onboardProjectionMod = require("./onboard-projection.cjs");
69
57
  const { REQUIRED_CODEBASE_MAP_FILES } = onboardProjectionMod;
70
58
  const clock_cjs_1 = require("./clock.cjs");
71
- const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
72
- const { defaultPhaseCleanCommitTimesMs } = verificationMod;
59
+ const { planningDir, withPlanningLock } = planningWorkspace;
73
60
  const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
74
61
  const { readStateHeadFreshness } = stateMod;
75
62
  /**
@@ -273,8 +260,8 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
273
260
  }
274
261
  /**
275
262
  * Issue #429 — negative-grep comment-text echo gate.
276
- * A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0)
277
- * must not also appear verbatim inside an <action> body, or the executor's commit-time
263
+ * A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0, or the
264
+ * negated spellings `! grep -q 'LIT' file` / `if ! grep ...`, #4541) must not also appear verbatim inside an <action> body, or the executor's commit-time
278
265
  * verify gate fails on the comment echo rather than a real regression. Conservative:
279
266
  * errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
280
267
  */
@@ -339,6 +326,12 @@ function splitShellSegments(line) {
339
326
  segments.push(current.trim());
340
327
  return segments.filter((s) => s !== '');
341
328
  }
329
+ /**
330
+ * The status-test alternation of a negated grep gate (`grep -q 'LIT' f; test $? -ne 0`): the one
331
+ * definition `statusNegatedTest` (the segment test) and `statusNegatedSpanRe` (the span removal)
332
+ * share, so the two cannot drift apart.
333
+ */
334
+ const STATUS_NEGATED_ALTERNATION = String.raw `(?:-ne\s+0|-eq\s+1|!=\s*0|==\s*1)`;
342
335
  function scanNegativeGrepCommentEcho(content) {
343
336
  const errors = [];
344
337
  const warnings = [];
@@ -376,6 +369,18 @@ function scanNegativeGrepCommentEcho(content) {
376
369
  // command, not the whole line) so a pasted verify command does not self-flag
377
370
  // while a prose echo on the same line is still caught.
378
371
  const cmdSpanRe = /grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
372
+ // #4541: the other spellings of a negative gate — `! grep ...` / `if ! grep ...` (the shell
373
+ // negates the exit status) and `grep ...; test $? -ne 0`. A pasted command in an <action> is
374
+ // likewise not an echo, so these spans are removed from the scanned action text too.
375
+ // The `!` is a command word: it follows a line start, whitespace, `;`, `(` or the `>` closing the
376
+ // `<automated>` tag, and is itself followed by whitespace (so `!=` and `!==` never match).
377
+ const negatedGrepRe = /(?:^|[\s;(>])!\s+grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
378
+ const negatedSpanRe = /(?:^|[\s;(>])!\s+grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n;&|<]*/g;
379
+ const statusNegatedTest = new RegExp(String.raw `\$\?\s*${STATUS_NEGATED_ALTERNATION}(?![\w.])`);
380
+ // The span between the grep literal and the status test is bounded (`{0,1000}`), like the other
381
+ // scans in this file, rather than a lazy unanchored run to the end of the line.
382
+ const statusNegatedSpanRe = new RegExp(String.raw `grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]{0,1000}?\$\?\s*${STATUS_NEGATED_ALTERNATION}(?![\w.])`, 'g');
383
+ const anyGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
379
384
  // Security scan: must see the FULL text up to the first </action> — including a
380
385
  // malformed inner <action> — so a grep-echo-0 trick cannot hide behind a
381
386
  // deliberately-unclosed tag. Use a bounded to-first-close scan (ReDoS-safe via
@@ -386,7 +391,9 @@ function scanNegativeGrepCommentEcho(content) {
386
391
  let acm;
387
392
  while ((acm = actionRe.exec(text)) !== null)
388
393
  actionZones.push(acm[1]);
389
- const scannableActionText = actionZones.map((zone) => zone.replace(cmdSpanRe, ' ')).join('\n');
394
+ const scannableActionText = actionZones
395
+ .map((zone) => zone.replace(cmdSpanRe, ' ').replace(negatedSpanRe, ' ').replace(statusNegatedSpanRe, ' '))
396
+ .join('\n');
390
397
  // 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
391
398
  // check echoes. Per-segment splitting keeps a positive gate (`== 1`) from
392
399
  // poisoning a negative gate (`== 0`) sharing the same physical line.
@@ -394,28 +401,52 @@ function scanNegativeGrepCommentEcho(content) {
394
401
  const seenWarn = new Set();
395
402
  const segments = text.split('\n').flatMap(splitShellSegments);
396
403
  for (const seg of segments) {
397
- if (!/grep(?:\s+-{1,2}[A-Za-z])/.test(seg) || !zeroCmp(seg))
404
+ if (!/grep\b/.test(seg))
398
405
  continue;
399
- countGrepRe.lastIndex = 0;
406
+ const countForm = /grep(?:\s+-{1,2}[A-Za-z])/.test(seg) && zeroCmp(seg);
407
+ const statusForm = statusNegatedTest.test(seg);
400
408
  const quotedLits = [];
401
409
  const bareLits = [];
402
- let m;
403
- while ((m = countGrepRe.exec(seg)) !== null) {
404
- if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
405
- continue; // need count, not invert (-cv is positive)
410
+ const harvest = (m) => {
406
411
  if (m[2] !== undefined)
407
412
  quotedLits.push(m[2]);
408
413
  else if (m[3] !== undefined)
409
414
  quotedLits.push(m[3]);
410
415
  else if (m[4] !== undefined && plausibleBare(m[4]))
411
416
  bareLits.push(m[4]);
417
+ };
418
+ let m;
419
+ if (countForm) {
420
+ countGrepRe.lastIndex = 0;
421
+ while ((m = countGrepRe.exec(seg)) !== null) {
422
+ if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
423
+ continue; // need count, not invert (-cv is positive)
424
+ harvest(m);
425
+ }
426
+ }
427
+ // `! grep -q 'LIT' f` / `if ! grep ...`: the negation is the shell's own, so any grep it
428
+ // negates is a negative gate whether or not it counts (an inverted grep is the positive one).
429
+ negatedGrepRe.lastIndex = 0;
430
+ while ((m = negatedGrepRe.exec(seg)) !== null) {
431
+ if (!optsHaveInvert(m[1]))
432
+ harvest(m);
433
+ }
434
+ // `grep -q 'LIT' f; test $? -ne 0`: the same gate with the status tested on the next command.
435
+ if (statusForm) {
436
+ anyGrepRe.lastIndex = 0;
437
+ while ((m = anyGrepRe.exec(seg)) !== null) {
438
+ if (!optsHaveInvert(m[1]))
439
+ harvest(m);
440
+ }
412
441
  }
442
+ if (quotedLits.length === 0 && bareLits.length === 0)
443
+ continue;
413
444
  for (const quoted of quotedLits) {
414
445
  if (!quoted || allow.has(quoted) || seenErr.has(quoted))
415
446
  continue;
416
447
  if (scannableActionText.includes(quoted)) {
417
448
  seenErr.add(quoted);
418
- errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
449
+ errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0, or a negated gate such as ! grep -q ...). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
419
450
  }
420
451
  }
421
452
  if (quotedLits.length === 0) {
@@ -854,8 +885,15 @@ function scanQuantitativeCriteria(content) {
854
885
  // Not a markdown table: the negated-pipe class matches a SHELL pipeline
855
886
  // stage boundary (git before the next `|`), the same shape the
856
887
  // #429/#968 scanners use; there is no table row to parse.
888
+ // #4774: the lookahead keeps `||` — a logical OR, the construct that
889
+ // HANDLES the failure (e.g. the `git cat-file -e <sha> || echo missing`
890
+ // ghost-control idiom, reachable here when prose apostrophes leave the
891
+ // segment's quote state unclosed) — quiet, while `|` and `|&` (a real
892
+ // pipeline stage, stderr-merged or not) still warn. Accepted trade-off:
893
+ // a QUOTED `||` literal ahead of a real pipe (``git grep 'a||b' f | wc -l``)
894
+ // also goes quiet — the lookahead cannot reach past the doubled pipe.
857
895
  // allow-adhoc-markdown: shell pipeline stage boundary, not a table cell (#4024)
858
- if (/\bgit\s+[a-z][^\n|]*\|/.test(seg)) {
896
+ if (/\bgit\s+[a-z][^\n|]*\|(?!\|)/.test(seg)) {
859
897
  record(warnings, 'R4', '[plan-criteria R4] A fallible `git` in a non-final pipeline stage is swallowed — the pipeline reports ' +
860
898
  'the last stage\'s status, so a broken command reads as clean. Capture the status first.', seenWarn);
861
899
  }
@@ -926,6 +964,13 @@ function extractPlanTaskInfos(content) {
926
964
  const nameArr = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(body, 'name');
927
965
  const hasName = nameArr.length > 0;
928
966
  const name = hasName ? nameArr[0].trim() : '';
967
+ // `(?:^|\s)` (not `\b`) so a hyphenated attribute ending in `auto_select`
968
+ // can never be mistaken for the real attribute — the same defensive
969
+ // anchor as extractOptionIds' `id` match below (#4095).
970
+ const autoSelectMatch = attrs.match(/(?:^|\s)auto_select\s*=\s*["']([^"']*)["']/);
971
+ const autoSelect = autoSelectMatch ? autoSelectMatch[1] : null;
972
+ const optionsArr = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(body, 'options');
973
+ const optionIds = optionsArr.length > 0 ? extractOptionIds(optionsArr[0]) : [];
929
974
  infos.push({
930
975
  name,
931
976
  type,
@@ -943,6 +988,8 @@ function extractPlanTaskInfos(content) {
943
988
  hasHowToVerify: /<how-to-verify[\s>]/.test(body),
944
989
  hasDecision: /<decision[\s>]/.test(body),
945
990
  hasOptions: /<options[\s>]/.test(body),
991
+ autoSelect,
992
+ optionIds,
946
993
  hasInstructions: /<instructions[\s>]/.test(body),
947
994
  hasVerification: /<verification[\s>]/.test(body),
948
995
  hasResumeSignal: /<resume-signal[\s>]/.test(body),
@@ -954,6 +1001,35 @@ function extractPlanTaskInfos(content) {
954
1001
  }
955
1002
  return infos;
956
1003
  }
1004
+ /**
1005
+ * Extract the `id` attribute of every `<option id="…">` opening tag found in
1006
+ * `optionsBody` (the inner text of one `<options>…</options>` block), in
1007
+ * document order. Bounded attribute scan (`[^>]{0,500}`), mirroring the same
1008
+ * ReDoS-safe idiom `extractPlanTaskInfos` uses for the `<task type="…">`
1009
+ * attribute string — this file's established pattern for reading an
1010
+ * attribute value without a general XML parser (#4095).
1011
+ */
1012
+ function extractOptionIds(optionsBody) {
1013
+ const ids = [];
1014
+ if (typeof optionsBody !== 'string' || optionsBody.length === 0)
1015
+ return ids;
1016
+ const OPTION_OPEN_RE = /<option(\s[^>]{0,500})?>/g;
1017
+ let match;
1018
+ while ((match = OPTION_OPEN_RE.exec(optionsBody)) !== null) {
1019
+ const attrs = match[1] ?? '';
1020
+ // `(?:^|\s)` (not `\b`) so a decoy attribute like `data-id="…"` inside
1021
+ // the same opening tag cannot be mistaken for the real `id` — `\b`
1022
+ // matches at the `-`→`i` boundary too, which `.match()`'s
1023
+ // first-hit-wins semantics would then silently prefer (#4095).
1024
+ const idMatch = attrs.match(/(?:^|\s)id\s*=\s*["']([^"']{1,200})["']/);
1025
+ if (idMatch)
1026
+ ids.push(idMatch[1]);
1027
+ if (match.index === OPTION_OPEN_RE.lastIndex) {
1028
+ OPTION_OPEN_RE.lastIndex++;
1029
+ }
1030
+ }
1031
+ return ids;
1032
+ }
957
1033
  function isCheckpointType(type) {
958
1034
  return type.startsWith('checkpoint:');
959
1035
  }
@@ -996,6 +1072,15 @@ function validatePlanTaskStructure(task) {
996
1072
  errors.push(`Task '${taskName}' missing <decision>`);
997
1073
  if (!task.hasOptions)
998
1074
  errors.push(`Task '${taskName}' missing <options>`);
1075
+ if (task.autoSelect !== null) {
1076
+ if (task.autoSelect.length === 0) {
1077
+ errors.push(`Task '${taskName}' auto_select is empty — name an <option id="…">`);
1078
+ }
1079
+ else if (task.hasOptions && !task.optionIds.includes(task.autoSelect)) {
1080
+ errors.push(`Task '${taskName}' auto_select="${task.autoSelect}" does not match any `
1081
+ + `<option id="…"> (available: ${task.optionIds.join(', ') || 'none'})`);
1082
+ }
1083
+ }
999
1084
  break;
1000
1085
  case 'checkpoint:human-action':
1001
1086
  if (!task.hasAction)
@@ -1023,6 +1108,45 @@ function validatePlanTaskStructure(task) {
1023
1108
  }
1024
1109
  return { errors, warnings };
1025
1110
  }
1111
+ // ─── Gate verb exit status (#5170, ADR-5057 §4) ──────────────────────────────────────────────────
1112
+ //
1113
+ // A verb that returns a verdict is evaluated by callers that branch on the exit status: exit 0 and
1114
+ // exit 1 are verdicts (the JSON on stdout is authoritative), any other status means the verb could
1115
+ // not look. Each verb builds a real GateVerdict and declares its exit from `gateExitOutcome` — no
1116
+ // verb picks its own code.
1117
+ /**
1118
+ * A verb's target file as evidence. An absent file is `none`; an EMPTY file is `found ''` (a plan
1119
+ * that exists but is empty is not "File not found"); any other failure is `unreadable`.
1120
+ */
1121
+ function readVerbFile(fullPath) {
1122
+ try {
1123
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1124
+ return content === null ? (0, gate_evidence_cjs_1.evidenceNone)() : (0, gate_evidence_cjs_1.evidenceFound)(content);
1125
+ }
1126
+ catch (err) {
1127
+ return (0, gate_evidence_cjs_1.evidenceFromError)(err, fullPath);
1128
+ }
1129
+ }
1130
+ /** Print a verb's verdict payload, then declare its exit. Declared after output(), which rewrites the pending-outcome cell. */
1131
+ function emitVerbVerdict(verdict, mode, raw, rawLabel) {
1132
+ output(verdict.payload, raw, rawLabel);
1133
+ (0, gate_exit_cjs_1.declareGateExit)(verdict, mode);
1134
+ }
1135
+ /** A verb whose result is valid/invalid: invalid is a negative verdict (exit 1 in status mode). */
1136
+ function validityVerdict(valid, payload) {
1137
+ return (0, gate_verdict_cjs_1.gateVerdict)(valid ? 'pass' : 'block', !valid, payload);
1138
+ }
1139
+ /**
1140
+ * The "could not evaluate" verdict for a verb's target file that is not `found` (outcome `unreadable`,
1141
+ * exit UNAVAILABLE — an absent file and an unreadable one alike). The caller emits it through
1142
+ * `emitVerbVerdict` on its own return path, so every path out of a verb visibly declares its exit.
1143
+ */
1144
+ function unreadableFileVerdict(evidence, displayPath) {
1145
+ const payload = evidence.kind === 'none'
1146
+ ? { error: 'File not found', path: displayPath }
1147
+ : { error: 'File unreadable', path: displayPath, read_error: evidence.reason };
1148
+ return (0, gate_verdict_cjs_1.gateUnreadable)(false, payload);
1149
+ }
1026
1150
  function cmdVerifyPlanStructure(cwd, filePath, raw) {
1027
1151
  if (!filePath) {
1028
1152
  error('file path required');
@@ -1031,17 +1155,18 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1031
1155
  error('file path contains null bytes');
1032
1156
  }
1033
1157
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
1034
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1035
- if (!content) {
1036
- output({ error: 'File not found', path: filePath }, raw);
1158
+ const read = readVerbFile(fullPath);
1159
+ if (read.kind !== 'found') {
1160
+ emitVerbVerdict(unreadableFileVerdict(read, filePath), 'status', raw);
1037
1161
  return;
1038
1162
  }
1163
+ const content = read.value;
1039
1164
  // #2701: fail loud on NUL/binary corruption before structure checks. A
1040
1165
  // structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
1041
1166
  // silently skipped by recursive/binary-skipping searchers downstream.
1042
1167
  const encErr = (0, validate_cjs_1.textEncodingError)(content, filePath);
1043
1168
  if (encErr) {
1044
- output({ valid: false, errors: [encErr] }, raw);
1169
+ emitVerbVerdict(validityVerdict(false, { valid: false, errors: [encErr] }), 'status', raw);
1045
1170
  return;
1046
1171
  }
1047
1172
  const fm = extractFrontmatter(content, fullPath);
@@ -1109,14 +1234,14 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1109
1234
  const quantScan = scanQuantitativeCriteria(content);
1110
1235
  errors.push(...quantScan.errors);
1111
1236
  warnings.push(...quantScan.warnings);
1112
- output({
1237
+ emitVerbVerdict(validityVerdict(errors.length === 0, {
1113
1238
  valid: errors.length === 0,
1114
1239
  errors,
1115
1240
  warnings,
1116
1241
  task_count: tasks.length,
1117
1242
  tasks,
1118
1243
  frontmatter_fields: Object.keys(fm),
1119
- }, raw, errors.length === 0 ? 'valid' : 'invalid');
1244
+ }), 'status', raw, errors.length === 0 ? 'valid' : 'invalid');
1120
1245
  }
1121
1246
  function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1122
1247
  if (!phase) {
@@ -1124,7 +1249,8 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1124
1249
  }
1125
1250
  const phaseInfoRaw = findPhaseInternal(cwd, phase);
1126
1251
  if (!phaseInfoRaw || !phaseInfoRaw['found']) {
1127
- output({ error: 'Phase not found', phase }, raw);
1252
+ // An unresolvable phase is "could not look" (#5170): there is no phase to evaluate.
1253
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Phase not found', phase }), 'status', raw);
1128
1254
  return;
1129
1255
  }
1130
1256
  const phaseInfo = phaseInfoRaw;
@@ -1140,12 +1266,14 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1140
1266
  // recognized naming forms — producing false "Plans without summaries" /
1141
1267
  // "Summaries without plans" for names it could not recognize as paired
1142
1268
  // (the same failure class #1988/#2648 fixed for the owner's own callers).
1143
- const scan = planScanMod.scanPhasePlans(phaseDir);
1144
- if (scan.scope === SCOPE.UNREADABLE) {
1145
- output({ error: 'Cannot read phase directory' }, raw);
1269
+ // Only SCOPE.COMPLETE is a real answer: an existing nested plans/ that could not be read (TRUNCATED)
1270
+ // would hand the check a short plan set and report the unseen plans as nothing (#5170).
1271
+ const scan = (0, gate_evidence_cjs_1.readPlanScanEvidence)(phaseDir);
1272
+ if (scan.kind === 'unreadable') {
1273
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Cannot read phase directory' }), 'status', raw);
1146
1274
  return;
1147
1275
  }
1148
- const { planFiles, summaryFiles } = scan;
1276
+ const { planFiles, summaryFiles } = scan.value;
1149
1277
  const incompletePlans = findUnsummarizedPlans(planFiles, summaryFiles);
1150
1278
  if (incompletePlans.length > 0) {
1151
1279
  errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
@@ -1154,7 +1282,7 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1154
1282
  if (orphanSummaries.length > 0) {
1155
1283
  warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
1156
1284
  }
1157
- output({
1285
+ emitVerbVerdict(validityVerdict(errors.length === 0, {
1158
1286
  complete: errors.length === 0,
1159
1287
  phase: phaseInfo['phase_number'],
1160
1288
  plan_count: planFiles.length,
@@ -1163,59 +1291,78 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1163
1291
  orphan_summaries: orphanSummaries,
1164
1292
  errors,
1165
1293
  warnings,
1166
- }, raw, errors.length === 0 ? 'complete' : 'incomplete');
1294
+ }), 'status', raw, errors.length === 0 ? 'complete' : 'incomplete');
1295
+ }
1296
+ // #4678: citations may carry a trailing line suffix (":42", ":1-20") that
1297
+ // describes a location inside the file, not part of the path itself.
1298
+ function stripLineSuffix(ref) {
1299
+ return ref.replace(/:\d+(?:-\d+)?$/, '');
1167
1300
  }
1168
1301
  function cmdVerifyReferences(cwd, filePath, raw) {
1169
1302
  if (!filePath) {
1170
1303
  error('file path required');
1171
1304
  }
1172
1305
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
1173
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1174
- if (!content) {
1175
- output({ error: 'File not found', path: filePath }, raw);
1306
+ const read = readVerbFile(fullPath);
1307
+ if (read.kind !== 'found') {
1308
+ emitVerbVerdict(unreadableFileVerdict(read, filePath), 'status', raw);
1176
1309
  return;
1177
1310
  }
1311
+ const content = read.value;
1178
1312
  const found = [];
1179
1313
  const missing = [];
1314
+ // A reference whose path could not be examined (EACCES on a parent ...) is neither found nor missing.
1315
+ const unexaminable = [];
1316
+ const probeRef = (cleanRef, resolved) => {
1317
+ const probe = (0, gate_evidence_cjs_1.statEvidence)(resolved);
1318
+ if (probe.kind === 'found')
1319
+ found.push(cleanRef);
1320
+ else if (probe.kind === 'none')
1321
+ missing.push(cleanRef);
1322
+ else
1323
+ unexaminable.push({ ref: cleanRef, reason: probe.reason });
1324
+ };
1180
1325
  const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
1181
1326
  for (const ref of atRefs) {
1182
1327
  const cleanRef = ref.slice(1);
1183
- const resolved = cleanRef.startsWith('~/')
1184
- ? node_path_1.default.join(process.env['HOME'] || '', cleanRef.slice(2))
1185
- : node_path_1.default.join(cwd, cleanRef);
1186
- if (node_fs_1.default.existsSync(resolved)) {
1187
- found.push(cleanRef);
1188
- }
1189
- else {
1190
- missing.push(cleanRef);
1191
- }
1328
+ const fsRef = stripLineSuffix(cleanRef);
1329
+ const resolved = fsRef.startsWith('~/')
1330
+ ? node_path_1.default.join(process.env['HOME'] || '', fsRef.slice(2))
1331
+ : node_path_1.default.join(cwd, fsRef);
1332
+ probeRef(cleanRef, resolved);
1192
1333
  }
1193
- const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || [];
1334
+ const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10}(?::\d+(?:-\d+)?)?)`/g) || [];
1194
1335
  for (const ref of backtickRefs) {
1195
1336
  const cleanRef = ref.slice(1, -1);
1196
1337
  if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{'))
1197
1338
  continue;
1198
- if (found.includes(cleanRef) || missing.includes(cleanRef))
1339
+ if (found.includes(cleanRef) || missing.includes(cleanRef) || unexaminable.some((u) => u.ref === cleanRef))
1199
1340
  continue;
1200
- const resolved = node_path_1.default.join(cwd, cleanRef);
1201
- if (node_fs_1.default.existsSync(resolved)) {
1202
- found.push(cleanRef);
1203
- }
1204
- else {
1205
- missing.push(cleanRef);
1206
- }
1341
+ probeRef(cleanRef, node_path_1.default.join(cwd, stripLineSuffix(cleanRef)));
1207
1342
  }
1208
- output({
1343
+ // Unexaminable references only matter when they could flip the verdict: with a reference already
1344
+ // missing the verdict is negative whatever they are; with none missing, "valid" cannot be claimed.
1345
+ if (missing.length === 0 && unexaminable.length > 0) {
1346
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Could not examine references', path: filePath, unreadable: unexaminable }), 'status', raw);
1347
+ return;
1348
+ }
1349
+ emitVerbVerdict(validityVerdict(missing.length === 0, {
1209
1350
  valid: missing.length === 0,
1210
1351
  found: found.length,
1211
1352
  missing,
1212
1353
  total: found.length + missing.length,
1213
- }, raw, missing.length === 0 ? 'valid' : 'invalid');
1354
+ }), 'status', raw, missing.length === 0 ? 'valid' : 'invalid');
1214
1355
  }
1215
1356
  function cmdVerifyCommits(cwd, hashes, raw) {
1216
1357
  if (!hashes || hashes.length === 0) {
1217
1358
  error('At least one commit hash required');
1218
1359
  }
1360
+ // A directory that is not a git work tree cannot answer "is this a commit": could not look, not "invalid".
1361
+ const inWorkTree = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd });
1362
+ if (inWorkTree.exitCode !== 0) {
1363
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Not a git repository' }), 'status', raw);
1364
+ return;
1365
+ }
1219
1366
  const valid = [];
1220
1367
  const invalid = [];
1221
1368
  for (const hash of hashes) {
@@ -1227,26 +1374,30 @@ function cmdVerifyCommits(cwd, hashes, raw) {
1227
1374
  invalid.push(hash);
1228
1375
  }
1229
1376
  }
1230
- output({
1377
+ emitVerbVerdict(validityVerdict(invalid.length === 0, {
1231
1378
  all_valid: invalid.length === 0,
1232
1379
  valid,
1233
1380
  invalid,
1234
1381
  total: hashes.length,
1235
- }, raw, invalid.length === 0 ? 'valid' : 'invalid');
1382
+ }), 'status', raw, invalid.length === 0 ? 'valid' : 'invalid');
1236
1383
  }
1237
1384
  function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1238
1385
  if (!planFilePath) {
1239
1386
  error('plan file path required');
1240
1387
  }
1241
1388
  const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
1242
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1243
- if (!content) {
1244
- output({ error: 'File not found', path: planFilePath }, raw);
1389
+ // #5170: no plan to read is "could not look" (UNAVAILABLE), never a pass. A plan that exists but is
1390
+ // empty WAS read: it has no artifacts block, which is a genuinely empty scope (below).
1391
+ const read = readVerbFile(fullPath);
1392
+ if (read.kind !== 'found') {
1393
+ emitVerbVerdict(unreadableFileVerdict(read, planFilePath), 'status', raw);
1245
1394
  return;
1246
1395
  }
1396
+ const content = read.value;
1247
1397
  const artifacts = parseMustHavesBlock(content, 'artifacts');
1248
1398
  if (artifacts.length === 0) {
1249
- output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
1399
+ // The plan was read and declares nothing to verify: NO_INPUT (ran, zero units in scope, genuinely).
1400
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateVerdict)('empty', false, { error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }), 'status', raw);
1250
1401
  return;
1251
1402
  }
1252
1403
  const results = [];
@@ -1257,27 +1408,66 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1257
1408
  if (!artPath)
1258
1409
  continue;
1259
1410
  const artFullPath = node_path_1.default.join(cwd, artPath);
1260
- const exists = node_fs_1.default.existsSync(artFullPath);
1411
+ // Typed evidence, not `fs.existsSync`: a path that cannot be examined (EACCES on a parent) is an
1412
+ // unreadable entry, not a missing one.
1413
+ const artStat = (0, gate_evidence_cjs_1.statEvidence)(artFullPath);
1414
+ const exists = artStat.kind === 'found';
1261
1415
  const check = { path: artPath, exists, issues: [], passed: false };
1416
+ // #4685: one artifact's I/O problem is that artifact's failure, never the
1417
+ // whole plan's. `safeReadFile` rethrows every errno except ENOENT, so before
1418
+ // this an unreadable entry — a directory most commonly, but equally an EACCES
1419
+ // file or a dangling mount — threw out of the loop and the command reported
1420
+ // NOTHING: not the offending entry, and not the plan's other, perfectly good
1421
+ // artifacts either. A check that disappears is worse than a check that fails,
1422
+ // because a failure is visible.
1423
+ //
1424
+ // #5170: the stat and the read are typed evidence (`statEvidence` / `readVerbFile`), so no `try`
1425
+ // is needed: an artifact that could not be examined or read is that artifact's failure, recorded
1426
+ // with its errno (an operator seeing EACCES acts differently from one seeing EIO) and left
1427
+ // `passed: false` — never a pass, so the verdict is negative (exit 1).
1262
1428
  if (exists) {
1263
- const fileContent = (0, shell_command_projection_cjs_1.platformReadSync)(artFullPath) || '';
1264
- const lineCount = fileContent.split('\n').length;
1265
- if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
1266
- check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
1267
- }
1268
- if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
1269
- check['issues'].push(`Missing pattern: ${artifact['contains']}`);
1429
+ // A directory is reported as its own kind of failure, distinct from
1430
+ // `File not found`: the path resolved, it simply is not the thing an
1431
+ // artifact entry can be checked against. Verifying a directory (matching
1432
+ // `contains:`/`min_lines:`/`exports:` across the files inside it) is a
1433
+ // feature decision, deliberately not made here.
1434
+ if (artStat.kind === 'found' && artStat.value.isDirectory()) {
1435
+ check['issues'].push('Not a file: path is a directory');
1270
1436
  }
1271
- if (artifact['exports']) {
1272
- const exports = Array.isArray(artifact['exports'])
1273
- ? artifact['exports']
1274
- : [artifact['exports']];
1275
- for (const exp of exports) {
1276
- if (!fileContent.includes(exp))
1277
- check['issues'].push(`Missing export: ${exp}`);
1437
+ else {
1438
+ // `safeReadFile` returns null on ENOENT, and `|| ''` would turn that
1439
+ // into an empty file — which an entry carrying only `path`/`provides`
1440
+ // would then PASS, having checked nothing. `statSync` just succeeded, so
1441
+ // `none` here means the artifact went away mid-check. Report that rather
1442
+ // than inheriting a pass from it.
1443
+ const artRead = readVerbFile(artFullPath);
1444
+ if (artRead.kind !== 'found') {
1445
+ check['issues'].push(artRead.kind === 'none' ? 'Unreadable: disappeared during check' : `Unreadable: ${artRead.reason}`);
1446
+ results.push(check);
1447
+ continue;
1278
1448
  }
1449
+ const fileContent = artRead.value;
1450
+ const lineCount = fileContent.split('\n').length;
1451
+ if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
1452
+ check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
1453
+ }
1454
+ if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
1455
+ check['issues'].push(`Missing pattern: ${artifact['contains']}`);
1456
+ }
1457
+ if (artifact['exports']) {
1458
+ const exports = Array.isArray(artifact['exports'])
1459
+ ? artifact['exports']
1460
+ : [artifact['exports']];
1461
+ for (const exp of exports) {
1462
+ if (!fileContent.includes(exp))
1463
+ check['issues'].push(`Missing export: ${exp}`);
1464
+ }
1465
+ }
1466
+ check['passed'] = check['issues'].length === 0;
1279
1467
  }
1280
- check['passed'] = check['issues'].length === 0;
1468
+ }
1469
+ else if (artStat.kind === 'unreadable') {
1470
+ check['issues'].push(`Unreadable: ${artStat.reason}`);
1281
1471
  }
1282
1472
  else {
1283
1473
  check['issues'].push('File not found');
@@ -1292,12 +1482,13 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1292
1482
  // no-vacuous-pass rule at src/uat-predicate.cts. The fully-empty block is still
1293
1483
  // caught earlier by the `artifacts.length === 0` guard and returns its error.
1294
1484
  const allPassed = results.length > 0 && passed === results.length;
1295
- output({
1485
+ // #5170: the exit status follows the verdict — a negative verdict is exit 1 (JSON unchanged).
1486
+ emitVerbVerdict(validityVerdict(allPassed, {
1296
1487
  all_passed: allPassed,
1297
1488
  passed,
1298
1489
  total: results.length,
1299
1490
  artifacts: results,
1300
- }, raw, allPassed ? 'valid' : 'invalid');
1491
+ }), 'status', raw, allPassed ? 'valid' : 'invalid');
1301
1492
  }
1302
1493
  /**
1303
1494
  * Returns a Set of file paths (relative to cwd) that are promised by plans in
@@ -1308,12 +1499,23 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1308
1499
  */
1309
1500
  function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
1310
1501
  const promised = new Set();
1311
- const { planFiles } = planScanMod.scanPhasePlans(phaseDir);
1312
- for (const planFile of planFiles) {
1502
+ // #5170: a plan set the scan did not fully see (an existing nested plans/ that could not be read), or
1503
+ // a plan that cannot be read, is "could not look" — it may be the plan that promises the file — and
1504
+ // is reported to the caller, never treated as "no plan promises it".
1505
+ let unreadable = null;
1506
+ const planSet = (0, gate_evidence_cjs_1.readPlanSetEvidence)(phaseDir);
1507
+ if (planSet.kind === 'unreadable')
1508
+ return { files: promised, unreadable: planSet.reason };
1509
+ for (const planFile of planSet.value) {
1313
1510
  const planFullPath = node_path_1.default.join(phaseDir, planFile);
1314
- const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
1315
- if (!planContent)
1511
+ const planRead = readVerbFile(planFullPath);
1512
+ if (planRead.kind === 'unreadable') {
1513
+ unreadable = unreadable ?? planRead.reason;
1514
+ continue;
1515
+ }
1516
+ if (planRead.kind === 'none' || !planRead.value)
1316
1517
  continue;
1518
+ const planContent = planRead.value;
1317
1519
  const fm = extractFrontmatter(planContent, planFullPath);
1318
1520
  const waveRaw = fm['wave'];
1319
1521
  const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
@@ -1330,21 +1532,23 @@ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
1330
1532
  promised.add(f.trim());
1331
1533
  }
1332
1534
  }
1333
- return promised;
1535
+ return { files: promised, unreadable };
1334
1536
  }
1335
1537
  function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1336
1538
  if (!planFilePath) {
1337
1539
  error('plan file path required');
1338
1540
  }
1339
1541
  const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
1340
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1341
- if (!content) {
1342
- output({ error: 'File not found', path: planFilePath }, raw);
1542
+ const read = readVerbFile(fullPath);
1543
+ if (read.kind !== 'found') {
1544
+ emitVerbVerdict(unreadableFileVerdict(read, planFilePath), 'status', raw);
1343
1545
  return;
1344
1546
  }
1547
+ const content = read.value;
1345
1548
  const keyLinks = parseMustHavesBlock(content, 'key_links');
1346
1549
  if (keyLinks.length === 0) {
1347
- output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
1550
+ // The plan was read and declares no key links: NO_INPUT, the same genuinely-empty scope as `verify artifacts`.
1551
+ emitVerbVerdict((0, gate_verdict_cjs_1.gateVerdict)('empty', false, { error: 'No must_haves.key_links found in frontmatter', path: planFilePath }), 'status', raw);
1348
1552
  return;
1349
1553
  }
1350
1554
  // Derive the current plan's wave number and phase directory for wave-aware
@@ -1397,28 +1601,30 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1397
1601
  results.push(check);
1398
1602
  continue;
1399
1603
  }
1400
- try {
1401
- sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(fromContained);
1402
- }
1403
- catch (err) {
1404
- // Report the errno only — never the message or path (untrusted `from:`
1405
- // can trigger EISDIR/EACCES, which platformReadSync re-throws for any
1406
- // non-ENOENT errno). A single bad link must not abort the whole command.
1407
- const code = err?.code ?? 'unknown';
1408
- check['detail'] = `Source read failed: ${code}`;
1604
+ // Report the errno only — never the message or path (untrusted `from:` can trigger
1605
+ // EISDIR/EACCES). A single bad link must not abort the whole command.
1606
+ const sourceRead = readVerbFile(fromContained);
1607
+ if (sourceRead.kind === 'unreadable') {
1608
+ check['detail'] = `Source read failed: ${sourceRead.reason}`;
1409
1609
  results.push(check);
1410
1610
  continue;
1411
1611
  }
1612
+ sourceContent = sourceRead.kind === 'found' ? sourceRead.value : null;
1412
1613
  }
1413
1614
  if (!sourceContent) {
1414
1615
  // Check if the missing file is promised by a plan at the same or later wave.
1415
1616
  const promised = getPromisedFiles();
1416
- const isPromised = fromPath.trim() !== '' && promised.has(fromPath.trim());
1617
+ const isPromised = fromPath.trim() !== '' && promised.files.has(fromPath.trim());
1417
1618
  if (isPromised) {
1418
1619
  check['pending'] = true;
1419
1620
  check['detail'] = 'Source file not yet created — declared in files_modified of a same-or-later-wave plan';
1420
1621
  pendingCount++;
1421
1622
  }
1623
+ else if (promised.unreadable !== null) {
1624
+ // The plans that could declare this file could not all be read: it is not known to be missing,
1625
+ // and the link is not verified — a failed link, never a pending or passing one.
1626
+ check['detail'] = `Source file not found, and the plans that could declare it could not be read: ${promised.unreadable}`;
1627
+ }
1422
1628
  else {
1423
1629
  check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
1424
1630
  }
@@ -1453,46 +1659,47 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1453
1659
  check['detail'] = `Pattern not verified (${reason})`;
1454
1660
  }
1455
1661
  else {
1456
- try {
1457
- if (pat.test(sourceContent)) {
1458
- check['verified'] = true;
1459
- check['detail'] = 'Pattern found in source';
1460
- }
1461
- else {
1462
- const toRaw = link['to'];
1463
- const toPath = typeof toRaw === 'string' ? toRaw : '';
1464
- let targetContent = null;
1465
- if (toPath !== '') {
1466
- // An empty/missing `to:` is a malformed plan, not a
1467
- // path-confinement violation — only a non-empty path that
1468
- // actually resolves outside the project is path_rejected.
1469
- const toContained = (0, security_cjs_2.tryWithinRoot)(toPath, cwd);
1470
- if (toContained === null) {
1471
- // Do not read a rejected `to:` — treat as no target content
1472
- // and do not echo result.error, which embeds absolute host
1473
- // paths.
1474
- check['path_rejected'] = 'to';
1475
- check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
1476
- }
1477
- else {
1478
- targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toContained);
1479
- }
1480
- }
1481
- if (targetContent && pat.test(targetContent)) {
1482
- check['verified'] = true;
1483
- check['detail'] = 'Pattern found in target';
1662
+ if (pat.test(sourceContent)) {
1663
+ check['verified'] = true;
1664
+ check['detail'] = 'Pattern found in source';
1665
+ }
1666
+ else {
1667
+ const toRaw = link['to'];
1668
+ const toPath = typeof toRaw === 'string' ? toRaw : '';
1669
+ let targetContent = null;
1670
+ let targetUnreadable = null;
1671
+ if (toPath !== '') {
1672
+ // An empty/missing `to:` is a malformed plan, not a
1673
+ // path-confinement violation — only a non-empty path that
1674
+ // actually resolves outside the project is path_rejected.
1675
+ const toContained = (0, security_cjs_2.tryWithinRoot)(toPath, cwd);
1676
+ if (toContained === null) {
1677
+ // Do not read a rejected `to:` — treat as no target content
1678
+ // and do not echo result.error, which embeds absolute host
1679
+ // paths.
1680
+ check['path_rejected'] = 'to';
1681
+ check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
1484
1682
  }
1485
- else if (!check['path_rejected']) {
1486
- check['detail'] = `Pattern "${link['pattern']}" not found in source or target`;
1683
+ else {
1684
+ // Report the errno only — never the message, which for an untrusted `to:` like "../.."
1685
+ // (EISDIR) would embed an absolute filesystem path.
1686
+ const targetRead = readVerbFile(toContained);
1687
+ if (targetRead.kind === 'unreadable')
1688
+ targetUnreadable = targetRead.reason;
1689
+ else if (targetRead.kind === 'found')
1690
+ targetContent = targetRead.value;
1487
1691
  }
1488
1692
  }
1489
- }
1490
- catch (err) {
1491
- // Report the errno only — never the full error/message, which for a
1492
- // re-thrown non-ENOENT platformReadSync failure (e.g. EISDIR from an
1493
- // untrusted `to:` like "../..") embeds an absolute filesystem path.
1494
- const code = err?.code ?? 'unknown';
1495
- check['detail'] = `Pattern check failed: ${code}`;
1693
+ if (targetUnreadable !== null) {
1694
+ check['detail'] = `Pattern check failed: ${targetUnreadable}`;
1695
+ }
1696
+ else if (targetContent && pat.test(targetContent)) {
1697
+ check['verified'] = true;
1698
+ check['detail'] = 'Pattern found in target';
1699
+ }
1700
+ else if (!check['path_rejected']) {
1701
+ check['detail'] = `Pattern "${link['pattern']}" not found in source or target`;
1702
+ }
1496
1703
  }
1497
1704
  }
1498
1705
  }
@@ -1522,13 +1729,13 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1522
1729
  // block still satisfies results.length > 0 and its #1202 non-hard-failing
1523
1730
  // semantics are unchanged — the floor only rejects the zero-result case.
1524
1731
  const allVerified = results.length > 0 && hardFailed === 0;
1525
- output({
1732
+ emitVerbVerdict(validityVerdict(allVerified, {
1526
1733
  all_verified: allVerified,
1527
1734
  verified,
1528
1735
  pending: pendingCount,
1529
1736
  total: results.length,
1530
1737
  links: results,
1531
- }, raw, allVerified ? 'valid' : 'invalid');
1738
+ }), 'status', raw, allVerified ? 'valid' : 'invalid');
1532
1739
  }
1533
1740
  /**
1534
1741
  * Wrapper-level fix-text table for `cmdValidateHealth`'s migrated
@@ -1692,6 +1899,9 @@ function cmdValidateHealth(cwd, options, raw) {
1692
1899
  // Diagnostic -> IssueEntry mapping contract this reproduces.
1693
1900
  const snapshot = buildPlanningSnapshot(cwd);
1694
1901
  const diagnostics = evaluateRules(snapshot);
1902
+ // #5118: an out-of-set verification report status is carried by the
1903
+ // snapshot (`verificationStatusError`) and reported as the W030 finding — a
1904
+ // diagnostics surface survives the defect it diagnoses, so this run exits 0.
1695
1905
  const errors = [];
1696
1906
  const warnings = [];
1697
1907
  const info = [];
@@ -1815,192 +2025,13 @@ function cmdValidateAgents(cwd, raw) {
1815
2025
  sandbox_posture: sandboxPosture,
1816
2026
  }, raw);
1817
2027
  }
1818
- // ─── Context drift (#3348) ───────────────────────────────────────────────────
1819
- /**
1820
- * Resolve a phase directory under `phasesDir` from a user-supplied `phaseArg`,
1821
- * via the canonical phase-directory matcher (phase-id.cjs::matchPhaseDirs) rather
1822
- * than a naive substring test — a bare `.includes(phaseArg)` lets a non-existent
1823
- * phase silently match a different phase whose directory name merely contains the
1824
- * requested token (e.g. "1" matching "11-expansion"). Falls back to an exact
1825
- * directory-name match. Returns null if neither resolves. (#1571, #2528)
1826
- */
1827
- function resolvePhaseDirByToken(phasesDir, phaseArg) {
1828
- const normalizedPhase = normalizePhaseName(phaseArg);
1829
- const dirEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1830
- const dirNames = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
1831
- const matched = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
1832
- if (matched)
1833
- return node_path_1.default.join(phasesDir, matched);
1834
- const contained = (0, security_cjs_2.tryWithinRoot)(phaseArg, phasesDir);
1835
- if (contained !== null && node_fs_1.default.existsSync(contained))
1836
- return contained;
1837
- return null;
1838
- }
1839
- /**
1840
- * Pure comparator: which of `entries` have an effective last-changed time
1841
- * STRICTLY BEFORE `contextEffectiveMs` (CONTEXT.md's own effective time)? Strict
1842
- * `<` is "stale" (matches findStaleVerificationSummary's own strict `>` convention
1843
- * for "newer than" elsewhere in this codebase — an artifact committed in the SAME
1844
- * commit/second as CONTEXT.md is in sync, not stale).
1845
- */
1846
- function computeContextDrift(contextEffectiveMs, entries) {
1847
- return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
1848
- }
1849
- function buildContextDriftMessage(staleArtifacts, phaseArg) {
1850
- const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
1851
- if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
1852
- parts.push(`Regenerate research: /gsd:plan-phase ${phaseArg} --research.`);
1853
- }
1854
- if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
1855
- parts.push('Regenerate patterns: delete the PATTERNS.md file, then re-run /gsd:plan-phase.');
1856
- }
1857
- if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
1858
- parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
1859
- }
1860
- parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
1861
- return parts.join(' ');
1862
- }
1863
- function cmdVerifyContextDrift(cwd, phaseArg, raw) {
1864
- if (!phaseArg) {
1865
- error('Usage: verify context-drift <phase>');
1866
- return;
1867
- }
1868
- const pDir = planningDir(cwd);
1869
- const phasesDir = node_path_1.default.join(pDir, 'phases');
1870
- const emitSkip = (reason, message = '') => {
1871
- output({ block: false, skipped: true, reason, stale_artifacts: [], message }, raw);
1872
- };
1873
- if (!node_fs_1.default.existsSync(phasesDir)) {
1874
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1875
- return;
1876
- }
1877
- // Same phase-directory resolution rule cmdVerifySchemaDrift uses (#1571, #2528):
1878
- // matchPhaseDirs, never a naive substring test.
1879
- const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
1880
- if (!phaseDir) {
1881
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1882
- return;
1883
- }
1884
- let phaseFiles;
1885
- try {
1886
- phaseFiles = node_fs_1.default.readdirSync(phaseDir).slice().sort();
1887
- }
1888
- catch {
1889
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1890
- return;
1891
- }
1892
- const contextFile = phaseFiles.find((f) => f.endsWith('-CONTEXT.md'));
1893
- if (!contextFile) {
1894
- emitSkip('no-context-md');
1895
- return;
1896
- }
1897
- const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
1898
- const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
1899
- const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
1900
- const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
1901
- const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
1902
- if (upstreamFiles.length === 0) {
1903
- emitSkip('no-upstream-artifacts');
1904
- return;
1905
- }
1906
- const allFiles = [contextFile, ...upstreamFiles];
1907
- const cleanCommitMs = defaultPhaseCleanCommitTimesMs(phaseDir, allFiles);
1908
- const effectiveTimeMs = (file) => cleanCommitMs.has(file)
1909
- ? cleanCommitMs.get(file)
1910
- : node_fs_1.default.statSync(node_path_1.default.join(phaseDir, file)).mtimeMs;
1911
- const contextMs = effectiveTimeMs(contextFile);
1912
- const driftEntries = upstreamFiles.map((f) => ({ file: f, effectiveMs: effectiveTimeMs(f) }));
1913
- const staleArtifacts = computeContextDrift(contextMs, driftEntries);
1914
- let wf;
1915
- try {
1916
- const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(pDir, 'config.json'), 'utf-8'));
1917
- wf = rawCfg['workflow'];
1918
- }
1919
- catch {
1920
- wf = undefined;
1921
- }
1922
- const action = wf?.context_drift_action === 'block' ? 'block' : 'warn';
1923
- const block = staleArtifacts.length > 0 && action === 'block';
1924
- const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
1925
- output({
1926
- block,
1927
- skipped: false,
1928
- stale_artifacts: staleArtifacts,
1929
- action,
1930
- message,
1931
- }, raw);
1932
- }
1933
- function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
1934
- if (!phaseArg) {
1935
- error('Usage: verify schema-drift <phase> [--skip]');
1936
- return;
1937
- }
1938
- const pDir = planningDir(cwd);
1939
- const phasesDir = node_path_1.default.join(pDir, 'phases');
1940
- if (!node_fs_1.default.existsSync(phasesDir)) {
1941
- output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
1942
- return;
1943
- }
1944
- // Resolve the phase directory with the canonical phase-directory matcher
1945
- // (phase-id.cjs::matchPhaseDirs), not a naive substring test. A bare
1946
- // `.includes(phaseArg)` lets a non-existent phase silently match a different
1947
- // phase whose directory name merely contains the requested token (e.g. "1"
1948
- // matching "11-expansion"), making the drift gate inspect the wrong phase.
1949
- // This shares the one selection rule with find-phase / verify
1950
- // phase-completeness rather than restating it. (#1571, #2528)
1951
- const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
1952
- if (!phaseDir) {
1953
- output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
1954
- return;
1955
- }
1956
- // #3183: canonical LIVE plan/summary sets (root+nested,
1957
- // status: superseded EXCLUDED) from the single owner, rather than a
1958
- // root-only readdirSync filter — a superseded plan's claimed
1959
- // files_modified is no longer treated as an expected drift target, and
1960
- // nested (#3139 layout) plans/summaries are no longer invisible to the
1961
- // drift check.
1962
- const { planFiles, summaryFiles } = planScanMod.scanPhasePlans(phaseDir);
1963
- const allFiles = [];
1964
- for (const pf of planFiles) {
1965
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, pf), 'utf-8');
1966
- const fmMatch = content.match(/files_modified:\s*\[([^\]]{0,8000})\]/);
1967
- if (fmMatch) {
1968
- const files = fmMatch[1].split(',').map((f) => f.trim()).filter(Boolean);
1969
- allFiles.push(...files);
1970
- }
1971
- }
1972
- let executionLog = '';
1973
- for (const sf of summaryFiles) {
1974
- executionLog += node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, sf), 'utf-8') + '\n';
1975
- }
1976
- const gitLog = (0, shell_command_projection_cjs_1.execGit)(['log', '--oneline', '--all', '-50'], { cwd });
1977
- if (gitLog.exitCode === 0) {
1978
- executionLog += '\n' + gitLog.stdout;
1979
- }
1980
- const result = (0, schema_detect_cjs_1.checkSchemaDrift)(allFiles, executionLog, { skipCheck: !!skipFlag });
1981
- const isSkipped = !!result['skipped'];
1982
- output({
1983
- // Uniform gate contract: `block` = true means "this gate's bad condition is met".
1984
- // When skipCheck is true (GSD_SKIP_SCHEMA_CHECK=true), the gate is bypassed —
1985
- // block must be false regardless of whether drift was detected.
1986
- // drift_detected and blocking are kept for compatibility.
1987
- block: isSkipped ? false : !!result['driftDetected'],
1988
- drift_detected: result['driftDetected'],
1989
- blocking: result['blocking'],
1990
- schema_files: result['schemaFiles'],
1991
- orms: result['orms'],
1992
- unpushed_orms: result['unpushedOrms'],
1993
- message: result['message'],
1994
- skipped: isSkipped,
1995
- }, raw);
1996
- }
1997
2028
  /**
1998
2029
  * Stamp `last_mapped_commit` (plus `last_mapped_at`) into the frontmatter of
1999
2030
  * every codebase-map document that exists on disk, using the current HEAD sha.
2000
2031
  *
2001
2032
  * #3418: `drift.cjs` shipped a correct `writeMappedCommit` with no production
2002
2033
  * caller, so no full `/gsd:map-codebase` run ever wrote the machine-readable
2003
- * baseline that `cmdVerifyCodebaseDrift` reads. The stamp lives in CODE rather
2034
+ * baseline that the codebase-drift gate (`gate-codebase-drift.cts`) reads. The stamp lives in CODE rather
2004
2035
  * than in a prose instruction to the mapper agent on purpose: an agent that
2005
2036
  * decides its work is already done skips a prose step silently, which is the
2006
2037
  * exact class of failure the stamp exists to detect.
@@ -2098,207 +2129,6 @@ function cmdStampCodebaseMap(cwd, raw, only) {
2098
2129
  emit(skip('exception: ' + (err instanceof Error ? err.message : String(err))));
2099
2130
  }
2100
2131
  }
2101
- function cmdVerifyCodebaseDrift(cwd, raw) {
2102
- // Non-hoisted: load-order matters for circular dep guard
2103
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
2104
- const drift = require('./drift.cjs');
2105
- const emit = (payload) => output(payload, raw);
2106
- try {
2107
- const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
2108
- const structurePath = node_path_1.default.join(codebaseDir, 'STRUCTURE.md');
2109
- if (!node_fs_1.default.existsSync(structurePath)) {
2110
- emit({
2111
- // Uniform gate contract: block = action_required (false when skipped).
2112
- block: false,
2113
- skipped: true,
2114
- reason: 'no-structure-md',
2115
- action_required: false,
2116
- directive: 'none',
2117
- elements: [],
2118
- });
2119
- return;
2120
- }
2121
- let structureMd;
2122
- try {
2123
- structureMd = node_fs_1.default.readFileSync(structurePath, 'utf-8');
2124
- }
2125
- catch (err) {
2126
- emit({
2127
- block: false,
2128
- skipped: true,
2129
- reason: 'cannot-read-structure-md: ' + (err instanceof Error ? err.message : String(err)),
2130
- action_required: false,
2131
- directive: 'none',
2132
- elements: [],
2133
- });
2134
- return;
2135
- }
2136
- const lastMapped = drift['readMappedCommit'](structurePath);
2137
- const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
2138
- if (revProbe.exitCode !== 0) {
2139
- emit({
2140
- block: false,
2141
- skipped: true,
2142
- reason: 'not-a-git-repo',
2143
- action_required: false,
2144
- directive: 'none',
2145
- elements: [],
2146
- });
2147
- return;
2148
- }
2149
- // #3418: an absent or unresolvable baseline means NO COMPARISON IS POSSIBLE.
2150
- // It is neither zero drift nor total drift, and reporting it as either is a
2151
- // lie the consumer cannot detect. The former fallback diffed HEAD against
2152
- // the empty tree, so every tracked file read as newly added and the gate
2153
- // reported maximum drift identically on every run -- which made a genuinely
2154
- // stale map indistinguishable from a fresh one, and let `spawn_mapper` fire
2155
- // a whole-repo remap while presenting itself as an incremental one.
2156
- if (!lastMapped) {
2157
- emit({
2158
- block: false,
2159
- skipped: true,
2160
- reason: 'no-mapped-commit',
2161
- action_required: false,
2162
- directive: 'none',
2163
- elements: [],
2164
- last_mapped_commit: null,
2165
- });
2166
- return;
2167
- }
2168
- const baseProbe = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', lastMapped], { cwd });
2169
- if (baseProbe.exitCode !== 0 || baseProbe.stdout.trim() !== 'commit') {
2170
- // A stamp git cannot resolve: history rewrite, GC, or a shallow clone.
2171
- // Distinct reason from 'no-mapped-commit' -- the map claims a baseline,
2172
- // this repository just cannot see it, which is an operator-actionable
2173
- // difference (re-map vs. unshallow). A resolvable non-commit (a tree or
2174
- // blob sha, a ref name) is the same class of bad baseline: git would
2175
- // happily diff against it and report drift against the wrong object.
2176
- emit({
2177
- block: false,
2178
- skipped: true,
2179
- reason: 'unresolvable-mapped-commit',
2180
- action_required: false,
2181
- directive: 'none',
2182
- elements: [],
2183
- last_mapped_commit: lastMapped,
2184
- });
2185
- return;
2186
- }
2187
- const base = lastMapped;
2188
- const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
2189
- if (diff.exitCode !== 0) {
2190
- emit({
2191
- block: false,
2192
- skipped: true,
2193
- reason: 'git-diff-failed',
2194
- action_required: false,
2195
- directive: 'none',
2196
- elements: [],
2197
- });
2198
- return;
2199
- }
2200
- // #3418: GSD's own planning artifacts are not codebase structure. A
2201
- // map-codebase run commits `.planning/codebase/*.md`, so a correctly
2202
- // stamped baseline would be re-poisoned by the very commit that carries
2203
- // the stamp -- the next gate invocation would report the map's own seven
2204
- // documents as seven new directories, back over the default threshold of
2205
- // three. Derived from planningRoot() rather than a hardcoded literal so a
2206
- // repoint of the planning root cannot leave this filter behind.
2207
- //
2208
- // `git diff --name-status` always prints repo-root-relative paths, so a cwd
2209
- // below the root needs the `sub/` prefix or the filter matches nothing.
2210
- // That prefix comes from git (`--show-prefix`: root-relative, forward
2211
- // slashes, trailing slash, empty at the root). The rejected alternative was
2212
- // path.relative(`--show-toplevel`, cwd), which mixes two path producers: on
2213
- // Windows os.tmpdir() hands back the 8.3 short form while git resolves the
2214
- // long one, so relative() between them yields a `../..` chain that matches
2215
- // nothing. The `.planning` half below is safe to compute with relative()
2216
- // because both of its sides are the same cwd string.
2217
- const prefixProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd });
2218
- const repoPrefix = prefixProbe.exitCode === 0 ? prefixProbe.stdout.trim() : '';
2219
- const planningPrefix = repoPrefix + node_path_1.default.relative(cwd, planningRoot(cwd)).split(node_path_1.default.sep).join('/') + '/';
2220
- const isPlanningArtifact = (file) => file.split('\\').join('/').startsWith(planningPrefix);
2221
- const added = [];
2222
- const modified = [];
2223
- const deleted = [];
2224
- for (const line of diff.stdout.split(/\r?\n/)) {
2225
- if (!line.trim())
2226
- continue;
2227
- const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
2228
- if (!m)
2229
- continue;
2230
- const status = m[1];
2231
- // execGit sets no core.quotepath config, so git's default `true` applies:
2232
- // any path containing non-ASCII bytes (or `"`, `\`, control bytes) is
2233
- // C-quoted — `"docs/\350\256\276…/overview.md"`. Capturing that verbatim
2234
- // garbles affected_paths/elements and makes isPathMapped compare the
2235
- // quoted prefix (`"docs`) against STRUCTURE.md, misclassifying DOCUMENTED
2236
- // directories as new_dir (#4081). Decode with the single owner of the
2237
- // git C-quote seam (worktree-safety.cjs); a non-quoted value — the plain
2238
- // ASCII common case — passes through untouched. Both capture groups are
2239
- // decoded: R/C lines carry old AND new paths, either may be quoted.
2240
- const file = decodeGitQuotedPath(m[3] || m[2]);
2241
- if (isPlanningArtifact(file))
2242
- continue;
2243
- if (status === 'A' || status === 'R' || status === 'C')
2244
- added.push(file);
2245
- else if (status === 'M')
2246
- modified.push(file);
2247
- else if (status === 'D')
2248
- deleted.push(file);
2249
- }
2250
- // loadConfig() returns a flattened object — there is no nested `workflow`
2251
- // key. Read the raw config.json directly to access workflow-scoped keys,
2252
- // matching the pattern used in check-command-router.cts:readWorkflowConfig.
2253
- let wf;
2254
- try {
2255
- const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf-8'));
2256
- wf = rawCfg['workflow'];
2257
- }
2258
- catch {
2259
- wf = undefined;
2260
- }
2261
- const threshold = Number.isInteger(wf?.drift_threshold) && wf?.drift_threshold >= 1
2262
- ? wf?.drift_threshold
2263
- : 3;
2264
- const action = wf?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
2265
- const driftResult = drift['detectDrift']({
2266
- addedFiles: added,
2267
- modifiedFiles: modified,
2268
- deletedFiles: deleted,
2269
- structureMd,
2270
- threshold,
2271
- action,
2272
- runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
2273
- });
2274
- const actionRequired = !!driftResult['actionRequired'];
2275
- emit({
2276
- // Uniform gate contract: block = action_required.
2277
- block: actionRequired,
2278
- skipped: !!driftResult['skipped'],
2279
- reason: driftResult['reason'] || null,
2280
- action_required: actionRequired,
2281
- directive: driftResult['directive'],
2282
- spawn_mapper: !!driftResult['spawnMapper'],
2283
- affected_paths: driftResult['affectedPaths'] || [],
2284
- elements: driftResult['elements'] || [],
2285
- threshold,
2286
- action,
2287
- last_mapped_commit: lastMapped,
2288
- message: driftResult['message'] || '',
2289
- });
2290
- }
2291
- catch (err) {
2292
- emit({
2293
- block: false,
2294
- skipped: true,
2295
- reason: 'exception: ' + (err && err instanceof Error ? err.message : String(err)),
2296
- action_required: false,
2297
- directive: 'none',
2298
- elements: [],
2299
- });
2300
- }
2301
- }
2302
2132
  module.exports = {
2303
2133
  scanNegativeGrepCommentEcho,
2304
2134
  scanFileWideNegativeGateConflict,
@@ -2314,10 +2144,6 @@ module.exports = {
2314
2144
  cmdValidateConsistency,
2315
2145
  cmdValidateHealth,
2316
2146
  cmdValidateAgents,
2317
- cmdVerifySchemaDrift,
2318
- cmdVerifyCodebaseDrift,
2319
- computeContextDrift,
2320
- cmdVerifyContextDrift,
2321
2147
  cmdStampCodebaseMap,
2322
2148
  STATE_HEAD_ADVISORY_COMMITS,
2323
2149
  };