@opengsd/gsd-core 1.15.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 (487) 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/agents/gsd-code-fixer.compact.md +1 -1
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.compact.md +5 -3
  7. package/agents/gsd-code-reviewer.md +8 -6
  8. package/agents/gsd-debug-session-manager.compact.md +1 -1
  9. package/agents/gsd-debug-session-manager.md +1 -1
  10. package/agents/gsd-debugger.md +2 -2
  11. package/agents/gsd-eval-auditor.compact.md +1 -1
  12. package/agents/gsd-eval-auditor.md +1 -1
  13. package/agents/gsd-executor.md +4 -4
  14. package/agents/gsd-intel-updater.compact.md +1 -1
  15. package/agents/gsd-intel-updater.md +1 -1
  16. package/agents/gsd-mempalace-curator.md +2 -2
  17. package/agents/gsd-phase-researcher.md +1 -1
  18. package/agents/gsd-plan-checker.md +7 -2
  19. package/agents/gsd-planner.md +3 -3
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.compact.md +21 -30
  25. package/agents/gsd-ui-auditor.md +42 -51
  26. package/agents/gsd-ui-researcher.compact.md +1 -1
  27. package/agents/gsd-ui-researcher.md +1 -1
  28. package/agents/gsd-verifier.md +31 -9
  29. package/bin/install.js +126 -196
  30. package/commands/gsd/add-tests.md +6 -1
  31. package/commands/gsd/ai-integration-phase.md +6 -1
  32. package/commands/gsd/audit-fix.md +5 -0
  33. package/commands/gsd/audit-milestone.md +6 -1
  34. package/commands/gsd/autonomous.md +5 -0
  35. package/commands/gsd/capture.md +8 -4
  36. package/commands/gsd/code-review.md +7 -2
  37. package/commands/gsd/complete-milestone.md +4 -0
  38. package/commands/gsd/config.md +7 -3
  39. package/commands/gsd/debug.md +11 -7
  40. package/commands/gsd/discuss-phase.md +7 -3
  41. package/commands/gsd/docs-update.md +12 -7
  42. package/commands/gsd/eval-review.md +6 -1
  43. package/commands/gsd/execute-phase.md +12 -7
  44. package/commands/gsd/extract-learnings.md +5 -0
  45. package/commands/gsd/fast.md +4 -0
  46. package/commands/gsd/forensics.md +5 -1
  47. package/commands/gsd/graphify.md +10 -6
  48. package/commands/gsd/health.md +5 -0
  49. package/commands/gsd/help.md +7 -2
  50. package/commands/gsd/import.md +7 -3
  51. package/commands/gsd/inbox.md +5 -0
  52. package/commands/gsd/ingest-docs.md +5 -1
  53. package/commands/gsd/manager.md +6 -1
  54. package/commands/gsd/map-codebase.md +7 -3
  55. package/commands/gsd/mempalace-capture.md +5 -1
  56. package/commands/gsd/mempalace-recall.md +5 -1
  57. package/commands/gsd/milestone-summary.md +5 -1
  58. package/commands/gsd/mvp-phase.md +8 -3
  59. package/commands/gsd/new-milestone.md +6 -1
  60. package/commands/gsd/new-project.md +5 -0
  61. package/commands/gsd/next.md +6 -1
  62. package/commands/gsd/ns-context.md +4 -0
  63. package/commands/gsd/ns-ideate.md +4 -0
  64. package/commands/gsd/ns-manage.md +4 -0
  65. package/commands/gsd/ns-project.md +4 -0
  66. package/commands/gsd/ns-review.md +4 -0
  67. package/commands/gsd/ns-workflow.md +4 -0
  68. package/commands/gsd/onboard.md +6 -1
  69. package/commands/gsd/pause-work.md +5 -1
  70. package/commands/gsd/phase.md +8 -4
  71. package/commands/gsd/plan-phase.md +6 -1
  72. package/commands/gsd/plan-review-convergence.md +5 -1
  73. package/commands/gsd/pr-branch.md +4 -0
  74. package/commands/gsd/profile-user.md +5 -1
  75. package/commands/gsd/progress.md +6 -1
  76. package/commands/gsd/quick-batch.md +20 -8
  77. package/commands/gsd/quick.md +12 -7
  78. package/commands/gsd/review.md +5 -1
  79. package/commands/gsd/secure-phase.md +6 -1
  80. package/commands/gsd/ship.md +5 -0
  81. package/commands/gsd/sketch.md +7 -2
  82. package/commands/gsd/spec-phase.md +5 -1
  83. package/commands/gsd/spike.md +8 -3
  84. package/commands/gsd/surface.md +5 -1
  85. package/commands/gsd/thread.md +4 -0
  86. package/commands/gsd/ui-phase.md +6 -1
  87. package/commands/gsd/ui-review.md +6 -1
  88. package/commands/gsd/ultraplan-phase.md +5 -1
  89. package/commands/gsd/undo.md +5 -1
  90. package/commands/gsd/update.md +6 -2
  91. package/commands/gsd/validate-phase.md +6 -1
  92. package/commands/gsd/verify-work.md +6 -1
  93. package/commands/gsd/workspace.md +7 -3
  94. package/gsd-core/bin/gsd-tools.cjs +142 -56
  95. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  96. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  97. package/gsd-core/bin/lib/audit.cjs +78 -44
  98. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  99. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  100. package/gsd-core/bin/lib/capability-registry.cjs +183 -103
  101. package/gsd-core/bin/lib/capability-validator.cjs +16 -0
  102. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  103. package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
  104. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  105. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  106. package/gsd-core/bin/lib/codex-agent-toml.cjs +11 -8
  107. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  108. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  109. package/gsd-core/bin/lib/commands.cjs +173 -170
  110. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  111. package/gsd-core/bin/lib/config.cjs +43 -12
  112. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  113. package/gsd-core/bin/lib/coverage.cjs +4 -8
  114. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  115. package/gsd-core/bin/lib/drift.cjs +177 -42
  116. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  117. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  118. package/gsd-core/bin/lib/frontmatter.cjs +413 -234
  119. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  120. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  121. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  122. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  123. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  124. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  125. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  126. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  127. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  128. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  129. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  130. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  131. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  132. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  133. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  134. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  135. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  136. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  137. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  138. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  139. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  140. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  141. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  142. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  143. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  144. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  145. package/gsd-core/bin/lib/init.cjs +162 -96
  146. package/gsd-core/bin/lib/install-engine.cjs +19 -35
  147. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  148. package/gsd-core/bin/lib/io.cjs +122 -3
  149. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  150. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  151. package/gsd-core/bin/lib/milestone.cjs +19 -1
  152. package/gsd-core/bin/lib/model-resolver.cjs +18 -18
  153. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  154. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  155. package/gsd-core/bin/lib/pattern.cjs +10 -0
  156. package/gsd-core/bin/lib/phase-command-router.cjs +11 -4
  157. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  158. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  159. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  160. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  161. package/gsd-core/bin/lib/phase.cjs +280 -80
  162. package/gsd-core/bin/lib/plan-document.cjs +93 -19
  163. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  164. package/gsd-core/bin/lib/planning-document.cjs +273 -40
  165. package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
  166. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  167. package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
  168. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  169. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  170. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  171. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  172. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  173. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  174. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  175. package/gsd-core/bin/lib/roadmap-command-router.cjs +13 -15
  176. package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
  177. package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
  178. package/gsd-core/bin/lib/roadmap.cjs +169 -72
  179. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
  180. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
  181. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  182. package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
  183. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
  184. package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
  185. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  186. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
  187. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  188. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  189. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  190. package/gsd-core/bin/lib/state-document.cjs +100 -22
  191. package/gsd-core/bin/lib/state.cjs +214 -105
  192. package/gsd-core/bin/lib/surface.cjs +2 -1
  193. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
  194. package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
  195. package/gsd-core/bin/lib/uat.cjs +424 -7
  196. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
  197. package/gsd-core/bin/lib/vendor/README.md +31 -9
  198. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  199. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  200. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  201. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  202. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  203. package/gsd-core/bin/lib/verification.cjs +1165 -314
  204. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  205. package/gsd-core/bin/lib/verify.cjs +286 -567
  206. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  207. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  208. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  209. package/gsd-core/bin/lib/workstream.cjs +11 -2
  210. package/gsd-core/bin/shared/config-defaults.manifest.json +5 -0
  211. package/gsd-core/bin/shared/config-schema.manifest.json +4 -0
  212. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  213. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  214. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  215. package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
  216. package/gsd-core/references/execute-phase-response-language.md +1 -1
  217. package/gsd-core/references/gsd-run-resolver.md +1 -1
  218. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  219. package/gsd-core/references/offer-next.md +1 -1
  220. package/gsd-core/references/planning-config.md +1 -1
  221. package/gsd-core/references/spidr-splitting.md +1 -1
  222. package/gsd-core/references/tdd.md +10 -4
  223. package/gsd-core/references/verifier-phase-gates.md +5 -2
  224. package/gsd-core/references/verify-mvp-mode.md +2 -2
  225. package/gsd-core/references/workstream-flag.md +33 -3
  226. package/gsd-core/templates/README.md +1 -1
  227. package/gsd-core/templates/UAT.md +17 -1
  228. package/gsd-core/templates/config.json +2 -11
  229. package/gsd-core/templates/verification-report.md +1 -1
  230. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  231. package/gsd-core/workflows/add-backlog.md +1 -1
  232. package/gsd-core/workflows/add-phase.md +8 -7
  233. package/gsd-core/workflows/add-tests.md +3 -2
  234. package/gsd-core/workflows/add-todo.md +4 -3
  235. package/gsd-core/workflows/ai-integration-phase.md +3 -2
  236. package/gsd-core/workflows/audit-fix.md +1 -1
  237. package/gsd-core/workflows/audit-milestone.md +4 -3
  238. package/gsd-core/workflows/audit-uat.md +1 -1
  239. package/gsd-core/workflows/autonomous.md +28 -18
  240. package/gsd-core/workflows/check-todos.md +6 -5
  241. package/gsd-core/workflows/cleanup.md +1 -1
  242. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  243. package/gsd-core/workflows/code-review-fix.md +5 -4
  244. package/gsd-core/workflows/code-review.md +84 -90
  245. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  246. package/gsd-core/workflows/complete-milestone.md +12 -7
  247. package/gsd-core/workflows/debug.md +30 -5
  248. package/gsd-core/workflows/diagnose-issues.md +3 -2
  249. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  250. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  252. package/gsd-core/workflows/discuss-phase.md +4 -3
  253. package/gsd-core/workflows/do.md +1 -1
  254. package/gsd-core/workflows/docs-update.md +4 -3
  255. package/gsd-core/workflows/edit-phase.md +4 -3
  256. package/gsd-core/workflows/eval-review.md +5 -3
  257. package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
  258. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
  259. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  260. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  261. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  263. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  264. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  265. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  266. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  267. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  268. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  269. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  270. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  271. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  272. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  273. package/gsd-core/workflows/execute-phase.md +78 -170
  274. package/gsd-core/workflows/execute-plan.md +13 -19
  275. package/gsd-core/workflows/explore.md +2 -2
  276. package/gsd-core/workflows/extract-learnings.md +3 -2
  277. package/gsd-core/workflows/fast.md +1 -1
  278. package/gsd-core/workflows/forensics.md +1 -1
  279. package/gsd-core/workflows/graduation.md +1 -1
  280. package/gsd-core/workflows/health.md +2 -1
  281. package/gsd-core/workflows/import.md +3 -2
  282. package/gsd-core/workflows/inbox.md +1 -1
  283. package/gsd-core/workflows/ingest-docs.md +1 -1
  284. package/gsd-core/workflows/insert-phase.md +4 -3
  285. package/gsd-core/workflows/list-seeds.md +1 -1
  286. package/gsd-core/workflows/list-workspaces.md +1 -1
  287. package/gsd-core/workflows/manager.md +5 -3
  288. package/gsd-core/workflows/map-codebase.md +4 -3
  289. package/gsd-core/workflows/milestone-summary.md +3 -2
  290. package/gsd-core/workflows/mvp-phase.md +14 -14
  291. package/gsd-core/workflows/new-milestone.md +10 -10
  292. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  293. package/gsd-core/workflows/new-project.md +1 -1
  294. package/gsd-core/workflows/new-workspace.md +1 -1
  295. package/gsd-core/workflows/next.md +1 -1
  296. package/gsd-core/workflows/pause-work.md +2 -2
  297. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  298. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  299. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  300. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  301. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +1 -1
  302. package/gsd-core/workflows/plan-phase.md +27 -18
  303. package/gsd-core/workflows/plan-review-convergence.md +1 -1
  304. package/gsd-core/workflows/plant-seed.md +1 -1
  305. package/gsd-core/workflows/pr-branch.md +1 -1
  306. package/gsd-core/workflows/profile-user.md +1 -1
  307. package/gsd-core/workflows/progress.md +19 -49
  308. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
  309. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  310. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  311. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  312. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  313. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  314. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  315. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  316. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  317. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  318. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  319. package/gsd-core/workflows/quick-batch.md +15 -10
  320. package/gsd-core/workflows/quick.md +42 -31
  321. package/gsd-core/workflows/remove-phase.md +3 -2
  322. package/gsd-core/workflows/remove-workspace.md +1 -1
  323. package/gsd-core/workflows/resume-project.md +3 -2
  324. package/gsd-core/workflows/review.md +3 -2
  325. package/gsd-core/workflows/scan.md +3 -2
  326. package/gsd-core/workflows/secure-phase.md +11 -12
  327. package/gsd-core/workflows/settings-advanced.md +1 -1
  328. package/gsd-core/workflows/settings-integrations.md +1 -1
  329. package/gsd-core/workflows/settings.md +1 -1
  330. package/gsd-core/workflows/ship.md +12 -12
  331. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  332. package/gsd-core/workflows/sketch.md +1 -1
  333. package/gsd-core/workflows/smart-entry.md +1 -1
  334. package/gsd-core/workflows/spec-phase.md +1 -1
  335. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  336. package/gsd-core/workflows/spike.md +1 -1
  337. package/gsd-core/workflows/stats.md +1 -1
  338. package/gsd-core/workflows/sync-skills.md +1 -1
  339. package/gsd-core/workflows/thread.md +2 -2
  340. package/gsd-core/workflows/transition.md +13 -23
  341. package/gsd-core/workflows/ui-phase.md +5 -4
  342. package/gsd-core/workflows/ui-review.md +4 -3
  343. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  344. package/gsd-core/workflows/undo.md +1 -1
  345. package/gsd-core/workflows/validate-phase.md +10 -12
  346. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  347. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  348. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  349. package/gsd-core/workflows/verify-work.md +68 -59
  350. package/hooks/dist/gsd-agent-isolation-guard.js +8 -0
  351. package/hooks/dist/gsd-check-update-worker.js +8 -0
  352. package/hooks/dist/gsd-check-update.js +8 -0
  353. package/hooks/dist/gsd-context-monitor.js +31 -8
  354. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  355. package/hooks/dist/gsd-secret-read-guard.js +147 -16
  356. package/hooks/dist/gsd-statusline.js +15 -7
  357. package/hooks/dist/gsd-update-banner.js +8 -0
  358. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  359. package/hooks/dist/gsd-workflow-guard.js +5 -4
  360. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  361. package/hooks/dist/lib/cli-exit.js +12 -0
  362. package/hooks/dist/lib/git-probe.js +17 -1
  363. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  364. package/hooks/gsd-agent-isolation-guard.js +8 -0
  365. package/hooks/gsd-check-update-worker.js +8 -0
  366. package/hooks/gsd-check-update.js +8 -0
  367. package/hooks/gsd-context-monitor.js +31 -8
  368. package/hooks/gsd-cursor-subagent-start.js +8 -0
  369. package/hooks/gsd-secret-read-guard.js +147 -16
  370. package/hooks/gsd-statusline.js +15 -7
  371. package/hooks/gsd-update-banner.js +8 -0
  372. package/hooks/gsd-windsurf-pre-write.js +11 -2
  373. package/hooks/gsd-workflow-guard.js +5 -4
  374. package/hooks/gsd-worktree-path-guard.js +6 -2
  375. package/hooks/hooks.json +5 -5
  376. package/hooks/lib/cli-exit.js +12 -0
  377. package/hooks/lib/git-probe.js +17 -1
  378. package/hooks/lib/isolation-sentinel.js +2 -2
  379. package/package.json +21 -4
  380. package/scripts/changeset/parse.cjs +52 -4
  381. package/scripts/ci-timeout-report.cjs +770 -4
  382. package/scripts/command-contract-helpers.cjs +12 -8
  383. package/scripts/docs-guard-registry.cjs +6 -0
  384. package/scripts/gen-features.cjs +13 -8
  385. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  386. package/scripts/gen-loop-host-contract.cjs +10 -1
  387. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  388. package/scripts/gen-plugin-skills.cjs +87 -1
  389. package/scripts/gen-research-agents.cjs +24 -31
  390. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  391. package/scripts/gen-test-timings.cjs +32 -7
  392. package/scripts/lib/cli-exit.cjs +12 -0
  393. package/scripts/lib/macos-conformance-tier.generated.cjs +20 -2
  394. package/scripts/lib/ndjson-reporter.cjs +28 -3
  395. package/scripts/lib/platform-conformance-tier.generated.cjs +34 -5
  396. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  397. package/scripts/lib/vendor-bundle.cjs +59 -0
  398. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  399. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  400. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  401. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  402. package/scripts/lint-descriptions.cjs +7 -3
  403. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +10 -0
  404. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  405. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  406. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  407. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  408. package/scripts/lint-phase-id-drift.cjs +29 -0
  409. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  410. package/scripts/lint-response-language-coverage.cjs +3 -0
  411. package/scripts/lint-skill-deps.cjs +7 -3
  412. package/scripts/lint-test-file-count.allowlist.json +11 -1
  413. package/scripts/lint-test-file-count.cjs +34 -1
  414. package/scripts/lint-vendored-deps.cjs +41 -5
  415. package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
  416. package/scripts/mutation-matrix.cjs +50 -5
  417. package/scripts/require-issue-link-policy.cjs +6 -2
  418. package/scripts/sync-runtime-launcher.cjs +184 -2
  419. package/scripts/verify-npm-publish.cjs +76 -20
  420. package/skills/gsd-add-tests/SKILL.md +6 -1
  421. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  422. package/skills/gsd-audit-fix/SKILL.md +5 -0
  423. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  424. package/skills/gsd-autonomous/SKILL.md +5 -0
  425. package/skills/gsd-capture/SKILL.md +8 -4
  426. package/skills/gsd-code-review/SKILL.md +7 -2
  427. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  428. package/skills/gsd-config/SKILL.md +8 -4
  429. package/skills/gsd-debug/SKILL.md +11 -7
  430. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  431. package/skills/gsd-docs-update/SKILL.md +12 -7
  432. package/skills/gsd-eval-review/SKILL.md +6 -1
  433. package/skills/gsd-execute-phase/SKILL.md +12 -7
  434. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  435. package/skills/gsd-fast/SKILL.md +4 -0
  436. package/skills/gsd-forensics/SKILL.md +5 -1
  437. package/skills/gsd-graphify/SKILL.md +10 -6
  438. package/skills/gsd-health/SKILL.md +5 -0
  439. package/skills/gsd-help/SKILL.md +7 -2
  440. package/skills/gsd-import/SKILL.md +7 -3
  441. package/skills/gsd-inbox/SKILL.md +5 -0
  442. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  443. package/skills/gsd-manager/SKILL.md +6 -1
  444. package/skills/gsd-map-codebase/SKILL.md +7 -3
  445. package/skills/gsd-mempalace-capture/SKILL.md +5 -1
  446. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  447. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  448. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  449. package/skills/gsd-new-milestone/SKILL.md +6 -1
  450. package/skills/gsd-new-project/SKILL.md +5 -0
  451. package/skills/gsd-next/SKILL.md +6 -1
  452. package/skills/gsd-ns-context/SKILL.md +4 -0
  453. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  454. package/skills/gsd-ns-manage/SKILL.md +4 -0
  455. package/skills/gsd-ns-project/SKILL.md +4 -0
  456. package/skills/gsd-ns-review/SKILL.md +4 -0
  457. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  458. package/skills/gsd-onboard/SKILL.md +6 -1
  459. package/skills/gsd-pause-work/SKILL.md +5 -1
  460. package/skills/gsd-phase/SKILL.md +8 -4
  461. package/skills/gsd-plan-phase/SKILL.md +6 -1
  462. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  463. package/skills/gsd-pr-branch/SKILL.md +4 -0
  464. package/skills/gsd-profile-user/SKILL.md +5 -1
  465. package/skills/gsd-progress/SKILL.md +6 -1
  466. package/skills/gsd-quick/SKILL.md +16 -10
  467. package/skills/gsd-quick-batch/SKILL.md +20 -8
  468. package/skills/gsd-review/SKILL.md +5 -1
  469. package/skills/gsd-review-backlog/SKILL.md +3 -2
  470. package/skills/gsd-secure-phase/SKILL.md +6 -1
  471. package/skills/gsd-ship/SKILL.md +5 -0
  472. package/skills/gsd-sketch/SKILL.md +7 -2
  473. package/skills/gsd-spec-phase/SKILL.md +5 -1
  474. package/skills/gsd-spike/SKILL.md +8 -3
  475. package/skills/gsd-surface/SKILL.md +5 -1
  476. package/skills/gsd-thread/SKILL.md +4 -0
  477. package/skills/gsd-ui-phase/SKILL.md +6 -1
  478. package/skills/gsd-ui-review/SKILL.md +6 -1
  479. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  480. package/skills/gsd-undo/SKILL.md +5 -1
  481. package/skills/gsd-update/SKILL.md +6 -2
  482. package/skills/gsd-validate-phase/SKILL.md +6 -1
  483. package/skills/gsd-verify-work/SKILL.md +6 -1
  484. package/skills/gsd-workspace/SKILL.md +7 -3
  485. package/skills/gsd-workstreams/SKILL.md +6 -6
  486. package/vscode/package.json +1 -1
  487. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
@@ -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) {
@@ -1077,6 +1108,45 @@ function validatePlanTaskStructure(task) {
1077
1108
  }
1078
1109
  return { errors, warnings };
1079
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
+ }
1080
1150
  function cmdVerifyPlanStructure(cwd, filePath, raw) {
1081
1151
  if (!filePath) {
1082
1152
  error('file path required');
@@ -1085,17 +1155,18 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1085
1155
  error('file path contains null bytes');
1086
1156
  }
1087
1157
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
1088
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1089
- if (!content) {
1090
- 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);
1091
1161
  return;
1092
1162
  }
1163
+ const content = read.value;
1093
1164
  // #2701: fail loud on NUL/binary corruption before structure checks. A
1094
1165
  // structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
1095
1166
  // silently skipped by recursive/binary-skipping searchers downstream.
1096
1167
  const encErr = (0, validate_cjs_1.textEncodingError)(content, filePath);
1097
1168
  if (encErr) {
1098
- output({ valid: false, errors: [encErr] }, raw);
1169
+ emitVerbVerdict(validityVerdict(false, { valid: false, errors: [encErr] }), 'status', raw);
1099
1170
  return;
1100
1171
  }
1101
1172
  const fm = extractFrontmatter(content, fullPath);
@@ -1163,14 +1234,14 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
1163
1234
  const quantScan = scanQuantitativeCriteria(content);
1164
1235
  errors.push(...quantScan.errors);
1165
1236
  warnings.push(...quantScan.warnings);
1166
- output({
1237
+ emitVerbVerdict(validityVerdict(errors.length === 0, {
1167
1238
  valid: errors.length === 0,
1168
1239
  errors,
1169
1240
  warnings,
1170
1241
  task_count: tasks.length,
1171
1242
  tasks,
1172
1243
  frontmatter_fields: Object.keys(fm),
1173
- }, raw, errors.length === 0 ? 'valid' : 'invalid');
1244
+ }), 'status', raw, errors.length === 0 ? 'valid' : 'invalid');
1174
1245
  }
1175
1246
  function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1176
1247
  if (!phase) {
@@ -1178,7 +1249,8 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1178
1249
  }
1179
1250
  const phaseInfoRaw = findPhaseInternal(cwd, phase);
1180
1251
  if (!phaseInfoRaw || !phaseInfoRaw['found']) {
1181
- 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);
1182
1254
  return;
1183
1255
  }
1184
1256
  const phaseInfo = phaseInfoRaw;
@@ -1194,12 +1266,14 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1194
1266
  // recognized naming forms — producing false "Plans without summaries" /
1195
1267
  // "Summaries without plans" for names it could not recognize as paired
1196
1268
  // (the same failure class #1988/#2648 fixed for the owner's own callers).
1197
- const scan = planScanMod.scanPhasePlans(phaseDir);
1198
- if (scan.scope === SCOPE.UNREADABLE) {
1199
- 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);
1200
1274
  return;
1201
1275
  }
1202
- const { planFiles, summaryFiles } = scan;
1276
+ const { planFiles, summaryFiles } = scan.value;
1203
1277
  const incompletePlans = findUnsummarizedPlans(planFiles, summaryFiles);
1204
1278
  if (incompletePlans.length > 0) {
1205
1279
  errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
@@ -1208,7 +1282,7 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1208
1282
  if (orphanSummaries.length > 0) {
1209
1283
  warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
1210
1284
  }
1211
- output({
1285
+ emitVerbVerdict(validityVerdict(errors.length === 0, {
1212
1286
  complete: errors.length === 0,
1213
1287
  phase: phaseInfo['phase_number'],
1214
1288
  plan_count: planFiles.length,
@@ -1217,7 +1291,7 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
1217
1291
  orphan_summaries: orphanSummaries,
1218
1292
  errors,
1219
1293
  warnings,
1220
- }, raw, errors.length === 0 ? 'complete' : 'incomplete');
1294
+ }), 'status', raw, errors.length === 0 ? 'complete' : 'incomplete');
1221
1295
  }
1222
1296
  // #4678: citations may carry a trailing line suffix (":42", ":1-20") that
1223
1297
  // describes a location inside the file, not part of the path itself.
@@ -1229,13 +1303,25 @@ function cmdVerifyReferences(cwd, filePath, raw) {
1229
1303
  error('file path required');
1230
1304
  }
1231
1305
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
1232
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1233
- if (!content) {
1234
- 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);
1235
1309
  return;
1236
1310
  }
1311
+ const content = read.value;
1237
1312
  const found = [];
1238
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
+ };
1239
1325
  const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
1240
1326
  for (const ref of atRefs) {
1241
1327
  const cleanRef = ref.slice(1);
@@ -1243,39 +1329,40 @@ function cmdVerifyReferences(cwd, filePath, raw) {
1243
1329
  const resolved = fsRef.startsWith('~/')
1244
1330
  ? node_path_1.default.join(process.env['HOME'] || '', fsRef.slice(2))
1245
1331
  : node_path_1.default.join(cwd, fsRef);
1246
- if (node_fs_1.default.existsSync(resolved)) {
1247
- found.push(cleanRef);
1248
- }
1249
- else {
1250
- missing.push(cleanRef);
1251
- }
1332
+ probeRef(cleanRef, resolved);
1252
1333
  }
1253
1334
  const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10}(?::\d+(?:-\d+)?)?)`/g) || [];
1254
1335
  for (const ref of backtickRefs) {
1255
1336
  const cleanRef = ref.slice(1, -1);
1256
1337
  if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{'))
1257
1338
  continue;
1258
- if (found.includes(cleanRef) || missing.includes(cleanRef))
1339
+ if (found.includes(cleanRef) || missing.includes(cleanRef) || unexaminable.some((u) => u.ref === cleanRef))
1259
1340
  continue;
1260
- const resolved = node_path_1.default.join(cwd, stripLineSuffix(cleanRef));
1261
- if (node_fs_1.default.existsSync(resolved)) {
1262
- found.push(cleanRef);
1263
- }
1264
- else {
1265
- missing.push(cleanRef);
1266
- }
1341
+ probeRef(cleanRef, node_path_1.default.join(cwd, stripLineSuffix(cleanRef)));
1267
1342
  }
1268
- 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, {
1269
1350
  valid: missing.length === 0,
1270
1351
  found: found.length,
1271
1352
  missing,
1272
1353
  total: found.length + missing.length,
1273
- }, raw, missing.length === 0 ? 'valid' : 'invalid');
1354
+ }), 'status', raw, missing.length === 0 ? 'valid' : 'invalid');
1274
1355
  }
1275
1356
  function cmdVerifyCommits(cwd, hashes, raw) {
1276
1357
  if (!hashes || hashes.length === 0) {
1277
1358
  error('At least one commit hash required');
1278
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
+ }
1279
1366
  const valid = [];
1280
1367
  const invalid = [];
1281
1368
  for (const hash of hashes) {
@@ -1287,26 +1374,30 @@ function cmdVerifyCommits(cwd, hashes, raw) {
1287
1374
  invalid.push(hash);
1288
1375
  }
1289
1376
  }
1290
- output({
1377
+ emitVerbVerdict(validityVerdict(invalid.length === 0, {
1291
1378
  all_valid: invalid.length === 0,
1292
1379
  valid,
1293
1380
  invalid,
1294
1381
  total: hashes.length,
1295
- }, raw, invalid.length === 0 ? 'valid' : 'invalid');
1382
+ }), 'status', raw, invalid.length === 0 ? 'valid' : 'invalid');
1296
1383
  }
1297
1384
  function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1298
1385
  if (!planFilePath) {
1299
1386
  error('plan file path required');
1300
1387
  }
1301
1388
  const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
1302
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1303
- if (!content) {
1304
- 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);
1305
1394
  return;
1306
1395
  }
1396
+ const content = read.value;
1307
1397
  const artifacts = parseMustHavesBlock(content, 'artifacts');
1308
1398
  if (artifacts.length === 0) {
1309
- 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);
1310
1401
  return;
1311
1402
  }
1312
1403
  const results = [];
@@ -1317,7 +1408,10 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1317
1408
  if (!artPath)
1318
1409
  continue;
1319
1410
  const artFullPath = node_path_1.default.join(cwd, artPath);
1320
- 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';
1321
1415
  const check = { path: artPath, exists, issues: [], passed: false };
1322
1416
  // #4685: one artifact's I/O problem is that artifact's failure, never the
1323
1417
  // whole plan's. `safeReadFile` rethrows every errno except ENOENT, so before
@@ -1327,67 +1421,56 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1327
1421
  // artifacts either. A check that disappears is worse than a check that fails,
1328
1422
  // because a failure is visible.
1329
1423
  //
1330
- // Scope of this guard, stated precisely (review nit): the `try` encloses the
1331
- // whole per-artifact body, but the only statements in it that can throw are the
1332
- // `statSync` and the read — the `min_lines`/`contains`/`exports` checks below
1333
- // are pure string operations. So this catches I/O, and nothing here is a
1334
- // deliberate guard around those criteria checks. A path `fs.existsSync` already
1335
- // rejected never reaches here either (that is the `File not found` branch), so
1336
- // this is not a claim to catch every way a path can be unusable.
1337
- try {
1338
- if (exists) {
1339
- // A directory is reported as its own kind of failure, distinct from
1340
- // `File not found`: the path resolved, it simply is not the thing an
1341
- // artifact entry can be checked against. Verifying a directory (matching
1342
- // `contains:`/`min_lines:`/`exports:` across the files inside it) is a
1343
- // feature decision, deliberately not made here.
1344
- if (node_fs_1.default.statSync(artFullPath).isDirectory()) {
1345
- check['issues'].push('Not a file: path is a directory');
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).
1428
+ if (exists) {
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');
1436
+ }
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;
1346
1448
  }
1347
- else {
1348
- // `safeReadFile` returns null on ENOENT, and `|| ''` would turn that
1349
- // into an empty file — which an entry carrying only `path`/`provides`
1350
- // would then PASS, having checked nothing. `statSync` just succeeded, so
1351
- // a null here means the artifact went away mid-check. Report that rather
1352
- // than inheriting a pass from it. (Pre-existing above this fix, reachable
1353
- // through the same race after `existsSync`; found in review.)
1354
- const rawContent = (0, shell_command_projection_cjs_1.platformReadSync)(artFullPath);
1355
- if (rawContent === null) {
1356
- check['issues'].push('Unreadable: disappeared during check');
1357
- results.push(check);
1358
- continue;
1359
- }
1360
- const fileContent = rawContent;
1361
- const lineCount = fileContent.split('\n').length;
1362
- if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
1363
- check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
1364
- }
1365
- if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
1366
- check['issues'].push(`Missing pattern: ${artifact['contains']}`);
1367
- }
1368
- if (artifact['exports']) {
1369
- const exports = Array.isArray(artifact['exports'])
1370
- ? artifact['exports']
1371
- : [artifact['exports']];
1372
- for (const exp of exports) {
1373
- if (!fileContent.includes(exp))
1374
- check['issues'].push(`Missing export: ${exp}`);
1375
- }
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}`);
1376
1464
  }
1377
- check['passed'] = check['issues'].length === 0;
1378
1465
  }
1379
- }
1380
- else {
1381
- check['issues'].push('File not found');
1466
+ check['passed'] = check['issues'].length === 0;
1382
1467
  }
1383
1468
  }
1384
- catch (err) {
1385
- // Unreadable for some other reason. Record the errno rather than a generic
1386
- // message — an operator seeing EACCES acts differently from one seeing EIO —
1387
- // and leave `passed` false.
1388
- const e = err;
1389
- check['issues'].push(`Unreadable: ${e.code || (e.message ?? String(err))}`);
1390
- check['passed'] = false;
1469
+ else if (artStat.kind === 'unreadable') {
1470
+ check['issues'].push(`Unreadable: ${artStat.reason}`);
1471
+ }
1472
+ else {
1473
+ check['issues'].push('File not found');
1391
1474
  }
1392
1475
  results.push(check);
1393
1476
  }
@@ -1399,12 +1482,13 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1399
1482
  // no-vacuous-pass rule at src/uat-predicate.cts. The fully-empty block is still
1400
1483
  // caught earlier by the `artifacts.length === 0` guard and returns its error.
1401
1484
  const allPassed = results.length > 0 && passed === results.length;
1402
- output({
1485
+ // #5170: the exit status follows the verdict — a negative verdict is exit 1 (JSON unchanged).
1486
+ emitVerbVerdict(validityVerdict(allPassed, {
1403
1487
  all_passed: allPassed,
1404
1488
  passed,
1405
1489
  total: results.length,
1406
1490
  artifacts: results,
1407
- }, raw, allPassed ? 'valid' : 'invalid');
1491
+ }), 'status', raw, allPassed ? 'valid' : 'invalid');
1408
1492
  }
1409
1493
  /**
1410
1494
  * Returns a Set of file paths (relative to cwd) that are promised by plans in
@@ -1415,12 +1499,23 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
1415
1499
  */
1416
1500
  function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
1417
1501
  const promised = new Set();
1418
- const { planFiles } = planScanMod.scanPhasePlans(phaseDir);
1419
- 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) {
1420
1510
  const planFullPath = node_path_1.default.join(phaseDir, planFile);
1421
- const planContent = (0, shell_command_projection_cjs_1.platformReadSync)(planFullPath);
1422
- if (!planContent)
1511
+ const planRead = readVerbFile(planFullPath);
1512
+ if (planRead.kind === 'unreadable') {
1513
+ unreadable = unreadable ?? planRead.reason;
1423
1514
  continue;
1515
+ }
1516
+ if (planRead.kind === 'none' || !planRead.value)
1517
+ continue;
1518
+ const planContent = planRead.value;
1424
1519
  const fm = extractFrontmatter(planContent, planFullPath);
1425
1520
  const waveRaw = fm['wave'];
1426
1521
  const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
@@ -1437,21 +1532,23 @@ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
1437
1532
  promised.add(f.trim());
1438
1533
  }
1439
1534
  }
1440
- return promised;
1535
+ return { files: promised, unreadable };
1441
1536
  }
1442
1537
  function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1443
1538
  if (!planFilePath) {
1444
1539
  error('plan file path required');
1445
1540
  }
1446
1541
  const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
1447
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
1448
- if (!content) {
1449
- 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);
1450
1545
  return;
1451
1546
  }
1547
+ const content = read.value;
1452
1548
  const keyLinks = parseMustHavesBlock(content, 'key_links');
1453
1549
  if (keyLinks.length === 0) {
1454
- 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);
1455
1552
  return;
1456
1553
  }
1457
1554
  // Derive the current plan's wave number and phase directory for wave-aware
@@ -1504,28 +1601,30 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1504
1601
  results.push(check);
1505
1602
  continue;
1506
1603
  }
1507
- try {
1508
- sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(fromContained);
1509
- }
1510
- catch (err) {
1511
- // Report the errno only — never the message or path (untrusted `from:`
1512
- // can trigger EISDIR/EACCES, which platformReadSync re-throws for any
1513
- // non-ENOENT errno). A single bad link must not abort the whole command.
1514
- const code = err?.code ?? 'unknown';
1515
- 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}`;
1516
1609
  results.push(check);
1517
1610
  continue;
1518
1611
  }
1612
+ sourceContent = sourceRead.kind === 'found' ? sourceRead.value : null;
1519
1613
  }
1520
1614
  if (!sourceContent) {
1521
1615
  // Check if the missing file is promised by a plan at the same or later wave.
1522
1616
  const promised = getPromisedFiles();
1523
- const isPromised = fromPath.trim() !== '' && promised.has(fromPath.trim());
1617
+ const isPromised = fromPath.trim() !== '' && promised.files.has(fromPath.trim());
1524
1618
  if (isPromised) {
1525
1619
  check['pending'] = true;
1526
1620
  check['detail'] = 'Source file not yet created — declared in files_modified of a same-or-later-wave plan';
1527
1621
  pendingCount++;
1528
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
+ }
1529
1628
  else {
1530
1629
  check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
1531
1630
  }
@@ -1560,46 +1659,47 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1560
1659
  check['detail'] = `Pattern not verified (${reason})`;
1561
1660
  }
1562
1661
  else {
1563
- try {
1564
- if (pat.test(sourceContent)) {
1565
- check['verified'] = true;
1566
- check['detail'] = 'Pattern found in source';
1567
- }
1568
- else {
1569
- const toRaw = link['to'];
1570
- const toPath = typeof toRaw === 'string' ? toRaw : '';
1571
- let targetContent = null;
1572
- if (toPath !== '') {
1573
- // An empty/missing `to:` is a malformed plan, not a
1574
- // path-confinement violation — only a non-empty path that
1575
- // actually resolves outside the project is path_rejected.
1576
- const toContained = (0, security_cjs_2.tryWithinRoot)(toPath, cwd);
1577
- if (toContained === null) {
1578
- // Do not read a rejected `to:` — treat as no target content
1579
- // and do not echo result.error, which embeds absolute host
1580
- // paths.
1581
- check['path_rejected'] = 'to';
1582
- check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
1583
- }
1584
- else {
1585
- targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toContained);
1586
- }
1587
- }
1588
- if (targetContent && pat.test(targetContent)) {
1589
- check['verified'] = true;
1590
- 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`;
1591
1682
  }
1592
- else if (!check['path_rejected']) {
1593
- 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;
1594
1691
  }
1595
1692
  }
1596
- }
1597
- catch (err) {
1598
- // Report the errno only — never the full error/message, which for a
1599
- // re-thrown non-ENOENT platformReadSync failure (e.g. EISDIR from an
1600
- // untrusted `to:` like "../..") embeds an absolute filesystem path.
1601
- const code = err?.code ?? 'unknown';
1602
- 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
+ }
1603
1703
  }
1604
1704
  }
1605
1705
  }
@@ -1629,13 +1729,13 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1629
1729
  // block still satisfies results.length > 0 and its #1202 non-hard-failing
1630
1730
  // semantics are unchanged — the floor only rejects the zero-result case.
1631
1731
  const allVerified = results.length > 0 && hardFailed === 0;
1632
- output({
1732
+ emitVerbVerdict(validityVerdict(allVerified, {
1633
1733
  all_verified: allVerified,
1634
1734
  verified,
1635
1735
  pending: pendingCount,
1636
1736
  total: results.length,
1637
1737
  links: results,
1638
- }, raw, allVerified ? 'valid' : 'invalid');
1738
+ }), 'status', raw, allVerified ? 'valid' : 'invalid');
1639
1739
  }
1640
1740
  /**
1641
1741
  * Wrapper-level fix-text table for `cmdValidateHealth`'s migrated
@@ -1799,6 +1899,9 @@ function cmdValidateHealth(cwd, options, raw) {
1799
1899
  // Diagnostic -> IssueEntry mapping contract this reproduces.
1800
1900
  const snapshot = buildPlanningSnapshot(cwd);
1801
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.
1802
1905
  const errors = [];
1803
1906
  const warnings = [];
1804
1907
  const info = [];
@@ -1922,192 +2025,13 @@ function cmdValidateAgents(cwd, raw) {
1922
2025
  sandbox_posture: sandboxPosture,
1923
2026
  }, raw);
1924
2027
  }
1925
- // ─── Context drift (#3348) ───────────────────────────────────────────────────
1926
- /**
1927
- * Resolve a phase directory under `phasesDir` from a user-supplied `phaseArg`,
1928
- * via the canonical phase-directory matcher (phase-id.cjs::matchPhaseDirs) rather
1929
- * than a naive substring test — a bare `.includes(phaseArg)` lets a non-existent
1930
- * phase silently match a different phase whose directory name merely contains the
1931
- * requested token (e.g. "1" matching "11-expansion"). Falls back to an exact
1932
- * directory-name match. Returns null if neither resolves. (#1571, #2528)
1933
- */
1934
- function resolvePhaseDirByToken(phasesDir, phaseArg) {
1935
- const normalizedPhase = normalizePhaseName(phaseArg);
1936
- const dirEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
1937
- const dirNames = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
1938
- const matched = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
1939
- if (matched)
1940
- return node_path_1.default.join(phasesDir, matched);
1941
- const contained = (0, security_cjs_2.tryWithinRoot)(phaseArg, phasesDir);
1942
- if (contained !== null && node_fs_1.default.existsSync(contained))
1943
- return contained;
1944
- return null;
1945
- }
1946
- /**
1947
- * Pure comparator: which of `entries` have an effective last-changed time
1948
- * STRICTLY BEFORE `contextEffectiveMs` (CONTEXT.md's own effective time)? Strict
1949
- * `<` is "stale" (matches findStaleVerificationSummary's own strict `>` convention
1950
- * for "newer than" elsewhere in this codebase — an artifact committed in the SAME
1951
- * commit/second as CONTEXT.md is in sync, not stale).
1952
- */
1953
- function computeContextDrift(contextEffectiveMs, entries) {
1954
- return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
1955
- }
1956
- function buildContextDriftMessage(staleArtifacts, phaseArg) {
1957
- const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
1958
- if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
1959
- parts.push(`Regenerate research: /gsd:plan-phase ${phaseArg} --research.`);
1960
- }
1961
- if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
1962
- parts.push('Regenerate patterns: delete the PATTERNS.md file, then re-run /gsd:plan-phase.');
1963
- }
1964
- if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
1965
- parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
1966
- }
1967
- parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
1968
- return parts.join(' ');
1969
- }
1970
- function cmdVerifyContextDrift(cwd, phaseArg, raw) {
1971
- if (!phaseArg) {
1972
- error('Usage: verify context-drift <phase>');
1973
- return;
1974
- }
1975
- const pDir = planningDir(cwd);
1976
- const phasesDir = node_path_1.default.join(pDir, 'phases');
1977
- const emitSkip = (reason, message = '') => {
1978
- output({ block: false, skipped: true, reason, stale_artifacts: [], message }, raw);
1979
- };
1980
- if (!node_fs_1.default.existsSync(phasesDir)) {
1981
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1982
- return;
1983
- }
1984
- // Same phase-directory resolution rule cmdVerifySchemaDrift uses (#1571, #2528):
1985
- // matchPhaseDirs, never a naive substring test.
1986
- const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
1987
- if (!phaseDir) {
1988
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1989
- return;
1990
- }
1991
- let phaseFiles;
1992
- try {
1993
- phaseFiles = node_fs_1.default.readdirSync(phaseDir).slice().sort();
1994
- }
1995
- catch {
1996
- emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
1997
- return;
1998
- }
1999
- const contextFile = phaseFiles.find((f) => f.endsWith('-CONTEXT.md'));
2000
- if (!contextFile) {
2001
- emitSkip('no-context-md');
2002
- return;
2003
- }
2004
- const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
2005
- const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
2006
- const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
2007
- const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
2008
- const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
2009
- if (upstreamFiles.length === 0) {
2010
- emitSkip('no-upstream-artifacts');
2011
- return;
2012
- }
2013
- const allFiles = [contextFile, ...upstreamFiles];
2014
- const cleanCommitMs = defaultPhaseCleanCommitTimesMs(phaseDir, allFiles);
2015
- const effectiveTimeMs = (file) => cleanCommitMs.has(file)
2016
- ? cleanCommitMs.get(file)
2017
- : node_fs_1.default.statSync(node_path_1.default.join(phaseDir, file)).mtimeMs;
2018
- const contextMs = effectiveTimeMs(contextFile);
2019
- const driftEntries = upstreamFiles.map((f) => ({ file: f, effectiveMs: effectiveTimeMs(f) }));
2020
- const staleArtifacts = computeContextDrift(contextMs, driftEntries);
2021
- let wf;
2022
- try {
2023
- const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(pDir, 'config.json'), 'utf-8'));
2024
- wf = rawCfg['workflow'];
2025
- }
2026
- catch {
2027
- wf = undefined;
2028
- }
2029
- const action = wf?.context_drift_action === 'block' ? 'block' : 'warn';
2030
- const block = staleArtifacts.length > 0 && action === 'block';
2031
- const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
2032
- output({
2033
- block,
2034
- skipped: false,
2035
- stale_artifacts: staleArtifacts,
2036
- action,
2037
- message,
2038
- }, raw);
2039
- }
2040
- function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
2041
- if (!phaseArg) {
2042
- error('Usage: verify schema-drift <phase> [--skip]');
2043
- return;
2044
- }
2045
- const pDir = planningDir(cwd);
2046
- const phasesDir = node_path_1.default.join(pDir, 'phases');
2047
- if (!node_fs_1.default.existsSync(phasesDir)) {
2048
- output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
2049
- return;
2050
- }
2051
- // Resolve the phase directory with the canonical phase-directory matcher
2052
- // (phase-id.cjs::matchPhaseDirs), not a naive substring test. A bare
2053
- // `.includes(phaseArg)` lets a non-existent phase silently match a different
2054
- // phase whose directory name merely contains the requested token (e.g. "1"
2055
- // matching "11-expansion"), making the drift gate inspect the wrong phase.
2056
- // This shares the one selection rule with find-phase / verify
2057
- // phase-completeness rather than restating it. (#1571, #2528)
2058
- const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
2059
- if (!phaseDir) {
2060
- output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
2061
- return;
2062
- }
2063
- // #3183: canonical LIVE plan/summary sets (root+nested,
2064
- // status: superseded EXCLUDED) from the single owner, rather than a
2065
- // root-only readdirSync filter — a superseded plan's claimed
2066
- // files_modified is no longer treated as an expected drift target, and
2067
- // nested (#3139 layout) plans/summaries are no longer invisible to the
2068
- // drift check.
2069
- const { planFiles, summaryFiles } = planScanMod.scanPhasePlans(phaseDir);
2070
- const allFiles = [];
2071
- for (const pf of planFiles) {
2072
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, pf), 'utf-8');
2073
- const fmMatch = content.match(/files_modified:\s*\[([^\]]{0,8000})\]/);
2074
- if (fmMatch) {
2075
- const files = fmMatch[1].split(',').map((f) => f.trim()).filter(Boolean);
2076
- allFiles.push(...files);
2077
- }
2078
- }
2079
- let executionLog = '';
2080
- for (const sf of summaryFiles) {
2081
- executionLog += node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, sf), 'utf-8') + '\n';
2082
- }
2083
- const gitLog = (0, shell_command_projection_cjs_1.execGit)(['log', '--oneline', '--all', '-50'], { cwd });
2084
- if (gitLog.exitCode === 0) {
2085
- executionLog += '\n' + gitLog.stdout;
2086
- }
2087
- const result = (0, schema_detect_cjs_1.checkSchemaDrift)(allFiles, executionLog, { skipCheck: !!skipFlag });
2088
- const isSkipped = !!result['skipped'];
2089
- output({
2090
- // Uniform gate contract: `block` = true means "this gate's bad condition is met".
2091
- // When skipCheck is true (GSD_SKIP_SCHEMA_CHECK=true), the gate is bypassed —
2092
- // block must be false regardless of whether drift was detected.
2093
- // drift_detected and blocking are kept for compatibility.
2094
- block: isSkipped ? false : !!result['driftDetected'],
2095
- drift_detected: result['driftDetected'],
2096
- blocking: result['blocking'],
2097
- schema_files: result['schemaFiles'],
2098
- orms: result['orms'],
2099
- unpushed_orms: result['unpushedOrms'],
2100
- message: result['message'],
2101
- skipped: isSkipped,
2102
- }, raw);
2103
- }
2104
2028
  /**
2105
2029
  * Stamp `last_mapped_commit` (plus `last_mapped_at`) into the frontmatter of
2106
2030
  * every codebase-map document that exists on disk, using the current HEAD sha.
2107
2031
  *
2108
2032
  * #3418: `drift.cjs` shipped a correct `writeMappedCommit` with no production
2109
2033
  * caller, so no full `/gsd:map-codebase` run ever wrote the machine-readable
2110
- * 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
2111
2035
  * than in a prose instruction to the mapper agent on purpose: an agent that
2112
2036
  * decides its work is already done skips a prose step silently, which is the
2113
2037
  * exact class of failure the stamp exists to detect.
@@ -2205,207 +2129,6 @@ function cmdStampCodebaseMap(cwd, raw, only) {
2205
2129
  emit(skip('exception: ' + (err instanceof Error ? err.message : String(err))));
2206
2130
  }
2207
2131
  }
2208
- function cmdVerifyCodebaseDrift(cwd, raw) {
2209
- // Non-hoisted: load-order matters for circular dep guard
2210
- // eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
2211
- const drift = require('./drift.cjs');
2212
- const emit = (payload) => output(payload, raw);
2213
- try {
2214
- const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
2215
- const structurePath = node_path_1.default.join(codebaseDir, 'STRUCTURE.md');
2216
- if (!node_fs_1.default.existsSync(structurePath)) {
2217
- emit({
2218
- // Uniform gate contract: block = action_required (false when skipped).
2219
- block: false,
2220
- skipped: true,
2221
- reason: 'no-structure-md',
2222
- action_required: false,
2223
- directive: 'none',
2224
- elements: [],
2225
- });
2226
- return;
2227
- }
2228
- let structureMd;
2229
- try {
2230
- structureMd = node_fs_1.default.readFileSync(structurePath, 'utf-8');
2231
- }
2232
- catch (err) {
2233
- emit({
2234
- block: false,
2235
- skipped: true,
2236
- reason: 'cannot-read-structure-md: ' + (err instanceof Error ? err.message : String(err)),
2237
- action_required: false,
2238
- directive: 'none',
2239
- elements: [],
2240
- });
2241
- return;
2242
- }
2243
- const lastMapped = drift['readMappedCommit'](structurePath);
2244
- const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
2245
- if (revProbe.exitCode !== 0) {
2246
- emit({
2247
- block: false,
2248
- skipped: true,
2249
- reason: 'not-a-git-repo',
2250
- action_required: false,
2251
- directive: 'none',
2252
- elements: [],
2253
- });
2254
- return;
2255
- }
2256
- // #3418: an absent or unresolvable baseline means NO COMPARISON IS POSSIBLE.
2257
- // It is neither zero drift nor total drift, and reporting it as either is a
2258
- // lie the consumer cannot detect. The former fallback diffed HEAD against
2259
- // the empty tree, so every tracked file read as newly added and the gate
2260
- // reported maximum drift identically on every run -- which made a genuinely
2261
- // stale map indistinguishable from a fresh one, and let `spawn_mapper` fire
2262
- // a whole-repo remap while presenting itself as an incremental one.
2263
- if (!lastMapped) {
2264
- emit({
2265
- block: false,
2266
- skipped: true,
2267
- reason: 'no-mapped-commit',
2268
- action_required: false,
2269
- directive: 'none',
2270
- elements: [],
2271
- last_mapped_commit: null,
2272
- });
2273
- return;
2274
- }
2275
- const baseProbe = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', lastMapped], { cwd });
2276
- if (baseProbe.exitCode !== 0 || baseProbe.stdout.trim() !== 'commit') {
2277
- // A stamp git cannot resolve: history rewrite, GC, or a shallow clone.
2278
- // Distinct reason from 'no-mapped-commit' -- the map claims a baseline,
2279
- // this repository just cannot see it, which is an operator-actionable
2280
- // difference (re-map vs. unshallow). A resolvable non-commit (a tree or
2281
- // blob sha, a ref name) is the same class of bad baseline: git would
2282
- // happily diff against it and report drift against the wrong object.
2283
- emit({
2284
- block: false,
2285
- skipped: true,
2286
- reason: 'unresolvable-mapped-commit',
2287
- action_required: false,
2288
- directive: 'none',
2289
- elements: [],
2290
- last_mapped_commit: lastMapped,
2291
- });
2292
- return;
2293
- }
2294
- const base = lastMapped;
2295
- const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
2296
- if (diff.exitCode !== 0) {
2297
- emit({
2298
- block: false,
2299
- skipped: true,
2300
- reason: 'git-diff-failed',
2301
- action_required: false,
2302
- directive: 'none',
2303
- elements: [],
2304
- });
2305
- return;
2306
- }
2307
- // #3418: GSD's own planning artifacts are not codebase structure. A
2308
- // map-codebase run commits `.planning/codebase/*.md`, so a correctly
2309
- // stamped baseline would be re-poisoned by the very commit that carries
2310
- // the stamp -- the next gate invocation would report the map's own seven
2311
- // documents as seven new directories, back over the default threshold of
2312
- // three. Derived from planningRoot() rather than a hardcoded literal so a
2313
- // repoint of the planning root cannot leave this filter behind.
2314
- //
2315
- // `git diff --name-status` always prints repo-root-relative paths, so a cwd
2316
- // below the root needs the `sub/` prefix or the filter matches nothing.
2317
- // That prefix comes from git (`--show-prefix`: root-relative, forward
2318
- // slashes, trailing slash, empty at the root). The rejected alternative was
2319
- // path.relative(`--show-toplevel`, cwd), which mixes two path producers: on
2320
- // Windows os.tmpdir() hands back the 8.3 short form while git resolves the
2321
- // long one, so relative() between them yields a `../..` chain that matches
2322
- // nothing. The `.planning` half below is safe to compute with relative()
2323
- // because both of its sides are the same cwd string.
2324
- const prefixProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd });
2325
- const repoPrefix = prefixProbe.exitCode === 0 ? prefixProbe.stdout.trim() : '';
2326
- const planningPrefix = repoPrefix + node_path_1.default.relative(cwd, planningRoot(cwd)).split(node_path_1.default.sep).join('/') + '/';
2327
- const isPlanningArtifact = (file) => file.split('\\').join('/').startsWith(planningPrefix);
2328
- const added = [];
2329
- const modified = [];
2330
- const deleted = [];
2331
- for (const line of diff.stdout.split(/\r?\n/)) {
2332
- if (!line.trim())
2333
- continue;
2334
- const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
2335
- if (!m)
2336
- continue;
2337
- const status = m[1];
2338
- // execGit sets no core.quotepath config, so git's default `true` applies:
2339
- // any path containing non-ASCII bytes (or `"`, `\`, control bytes) is
2340
- // C-quoted — `"docs/\350\256\276…/overview.md"`. Capturing that verbatim
2341
- // garbles affected_paths/elements and makes isPathMapped compare the
2342
- // quoted prefix (`"docs`) against STRUCTURE.md, misclassifying DOCUMENTED
2343
- // directories as new_dir (#4081). Decode with the single owner of the
2344
- // git C-quote seam (worktree-safety.cjs); a non-quoted value — the plain
2345
- // ASCII common case — passes through untouched. Both capture groups are
2346
- // decoded: R/C lines carry old AND new paths, either may be quoted.
2347
- const file = decodeGitQuotedPath(m[3] || m[2]);
2348
- if (isPlanningArtifact(file))
2349
- continue;
2350
- if (status === 'A' || status === 'R' || status === 'C')
2351
- added.push(file);
2352
- else if (status === 'M')
2353
- modified.push(file);
2354
- else if (status === 'D')
2355
- deleted.push(file);
2356
- }
2357
- // loadConfig() returns a flattened object — there is no nested `workflow`
2358
- // key. Read the raw config.json directly to access workflow-scoped keys,
2359
- // matching the pattern used in check-command-router.cts:readWorkflowConfig.
2360
- let wf;
2361
- try {
2362
- const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf-8'));
2363
- wf = rawCfg['workflow'];
2364
- }
2365
- catch {
2366
- wf = undefined;
2367
- }
2368
- const threshold = Number.isInteger(wf?.drift_threshold) && wf?.drift_threshold >= 1
2369
- ? wf?.drift_threshold
2370
- : 3;
2371
- const action = wf?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
2372
- const driftResult = drift['detectDrift']({
2373
- addedFiles: added,
2374
- modifiedFiles: modified,
2375
- deletedFiles: deleted,
2376
- structureMd,
2377
- threshold,
2378
- action,
2379
- runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
2380
- });
2381
- const actionRequired = !!driftResult['actionRequired'];
2382
- emit({
2383
- // Uniform gate contract: block = action_required.
2384
- block: actionRequired,
2385
- skipped: !!driftResult['skipped'],
2386
- reason: driftResult['reason'] || null,
2387
- action_required: actionRequired,
2388
- directive: driftResult['directive'],
2389
- spawn_mapper: !!driftResult['spawnMapper'],
2390
- affected_paths: driftResult['affectedPaths'] || [],
2391
- elements: driftResult['elements'] || [],
2392
- threshold,
2393
- action,
2394
- last_mapped_commit: lastMapped,
2395
- message: driftResult['message'] || '',
2396
- });
2397
- }
2398
- catch (err) {
2399
- emit({
2400
- block: false,
2401
- skipped: true,
2402
- reason: 'exception: ' + (err && err instanceof Error ? err.message : String(err)),
2403
- action_required: false,
2404
- directive: 'none',
2405
- elements: [],
2406
- });
2407
- }
2408
- }
2409
2132
  module.exports = {
2410
2133
  scanNegativeGrepCommentEcho,
2411
2134
  scanFileWideNegativeGateConflict,
@@ -2421,10 +2144,6 @@ module.exports = {
2421
2144
  cmdValidateConsistency,
2422
2145
  cmdValidateHealth,
2423
2146
  cmdValidateAgents,
2424
- cmdVerifySchemaDrift,
2425
- cmdVerifyCodebaseDrift,
2426
- computeContextDrift,
2427
- cmdVerifyContextDrift,
2428
2147
  cmdStampCodebaseMap,
2429
2148
  STATE_HEAD_ADVISORY_COMMITS,
2430
2149
  };