@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -0,0 +1,933 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the PHASE-COMPLETION predicate (epic
6
+ * #3180, issue #3186, ADR-3180 Decision 4, spec §7.4).
7
+ *
8
+ * The derivation this guard protects is "is phase P complete?", under the
9
+ * DISK-STRICT rule §7.4 now locks (amended on this branch, commit
10
+ * af92fd4c9, per the #2957 maintainer decision): `readVerificationStatus`
11
+ * is called UNCONDITIONALLY (no plan-count precondition), and a ticked
12
+ * ROADMAP checkbox carries NO machine authority over disk state. The
13
+ * designated owner is `src/verification.cts` · `isPhaseComplete` (Decision
14
+ * 1). Per ADR-3180 Amendment 3's standing rule this guard was written BEFORE
15
+ * any src/ file was touched (guard-first discovery), with
16
+ * `FUNCTION_SCOPED_EXEMPTIONS` below INTENTIONALLY EMPTY at that point. The
17
+ * implementation step (issue #3186) has since landed `isPhaseComplete` and
18
+ * migrated its call sites onto it, and populated the map — see its own
19
+ * comment immediately above its definition for the per-entry reasoning,
20
+ * including two DECLARED DEVIATIONS (`buildPhaseCompletionProjection`'s
21
+ * retained `implementation_complete` and `buildWorkstreamInventory`'s
22
+ * pure-projection boundary) not named by the design doc's own
23
+ * DO-NOT-MIGRATE list.
24
+ *
25
+ * Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
26
+ * WHOLE `src/` TREE, never an allowlist of known files. Per Decision 4(d)
27
+ * that surface is widened further: `src/` alone is itself the forbidden
28
+ * allowlist, one directory wide, so this guard ALSO scans the prompt-layer
29
+ * markdown (`gsd-core/workflows`, `commands`, `agents`, `skills`) — see the
30
+ * "PROMPT-LAYER PROSE DETECTION" section below.
31
+ *
32
+ * FOUR INDEPENDENT SHAPES are detected, matching issue #3186's dispatch (shape
33
+ * (d) added by the Phase 4 follow-up that closed the `cmdStateSync` /
34
+ * `src/state.cts` gap the remote matrix exposed — see shape (d)'s own header
35
+ * below):
36
+ *
37
+ * (a) CHECKBOX-DERIVED COMPLETION — a ROADMAP `- [x] Phase N` tick treated
38
+ * as completion evidence. Categorically wrong under disk-strict
39
+ * (§7.4's DECIDED resolution: "a ticked ROADMAP checkbox is a human
40
+ * annotation with no machine authority"). Detected as the PAIRED
41
+ * shape actually observed twice in this tree: an `if` statement whose
42
+ * condition references a roadmap/checkbox-derived "complete" boolean
43
+ * AND tests some status variable `!== 'complete'`, followed (within a
44
+ * small bounded window — this pairs ONE conditional with its OWN
45
+ * consequent statement, the same tight-pairing role
46
+ * `lint-state-field-drift.cjs`'s `LADDER_WINDOW_LINES` plays, NOT the
47
+ * big function-scoped co-occurrence Decision 4(a)'s Goodhart lesson
48
+ * targets) by an assignment of that SAME variable to the literal
49
+ * `'complete'`.
50
+ * (b) PLAN-COUNT PRECONDITION GATING A VERIFICATION READ — §7.4 names this
51
+ * exactly as `buildPhaseCompletionProjection`'s divergence: a
52
+ * `planCount > 0`-shaped gate that decides WHETHER
53
+ * `readVerificationStatus(` runs at all, rather than calling it
54
+ * unconditionally. Detected as FUNCTION-SCOPED co-occurrence (no
55
+ * window — Decision 4(a)'s Goodhart lesson, and Phase 5's "bounded
56
+ * window missed 7 of 14 copies" trap) of a count-gate ANYWHERE in the
57
+ * function's own body, together with a ternary- OR block/`if`-gated
58
+ * call to `readVerificationStatus(` (a call that is NOT the function's
59
+ * unconditional top-level statement) — the ternary form is a same-line
60
+ * `?` before the call; the block form is a call sitting inside the
61
+ * BODY of an `if (...)` whose OWN condition matches the count-gate
62
+ * (`findIfCountGateBlockLines`), which also catches `if (planCount >
63
+ * 0) { x = readVerificationStatus(...) }` — a #3186 review finding
64
+ * (4a): the prior version matched only the same-line ternary and
65
+ * produced zero hits on the block form. Known, disclosed limit: a
66
+ * multi-line `if` condition whose opening `{` lands on a later line
67
+ * than the condition's own closing `)` is not detected (every
68
+ * count-gate condition actually present in this tree is one line).
69
+ * (c) LOCAL RE-IMPLEMENTATION OF "COMPLETE" FROM COUNTS — a
70
+ * `summaryCount >= planCount`-shaped comparison (either operand
71
+ * order, or the algebraic `summaryCount - planCount >= 0` restatement
72
+ * and its mirror), computed locally instead of calling the owner.
73
+ * Known, disclosed limit: further algebraic restatements (an
74
+ * intermediate difference variable, `Math.max`/`!()`-wrapped forms,
75
+ * etc.) are not reliably matchable by a bounded, non-backtracking
76
+ * regex and are not attempted — see the regexes' own comment. This is
77
+ * the single sharpest textual signature this epic's divergent copies
78
+ * share: `buildPhaseCompletionProjection`, `buildStateFrontmatter`
79
+ * (via `scanPhasePlans`'s own `completed` field),
80
+ * `cmdRoadmapAnalyze`, `cmdRoadmapUpdatePlanProgress`, and
81
+ * `buildWorkstreamInventory` all independently hand-roll this exact
82
+ * comparison.
83
+ * (d) A BARE FIELD READ OF `scanPhasePlans(...).completed` OUTSIDE THE
84
+ * OWNER (`src/plan-scan.cts`) — the shape the remote matrix exposed:
85
+ * `cmdStateSync` (`src/state.cts`, now fixed) destructured
86
+ * `scanPhasePlans(dirPath).completed` directly and used it AS a
87
+ * completion verdict, with no comparison for shapes (a)/(b)/(c) to
88
+ * catch — a bare property read is not a re-derivation shape any of the
89
+ * other three detectors match. `scanPhasePlans` legitimately EXPOSES
90
+ * `.completed` (it is Phase 1's own owner for "are all plans
91
+ * summarized?", a real and distinct question from "is the phase
92
+ * complete?"), so reading the field is not inherently wrong — USING it
93
+ * as a completion verdict is. That distinction is DATA-FLOW (what the
94
+ * caller does with the value), which no textual guard can decide.
95
+ * KNOWN, DISCLOSED, HONEST LIMIT: this detector cannot tell a
96
+ * legitimate "are summaries caught up with plans" read from an illegal
97
+ * "is the phase complete" read — it flags EVERY read of `.completed`
98
+ * off a `scanPhasePlans(` result outside `src/plan-scan.cts` and
99
+ * relies on `FUNCTION_SCOPED_EXEMPTIONS` carrying a WRITTEN REASON per
100
+ * exempted function (see that map's own comment for each current
101
+ * entry's reasoning) rather than silently deciding the question for
102
+ * itself. Detected in three independent textual forms, ALL
103
+ * function-scoped (Decision 4(a), no line window — see below):
104
+ * - the direct chained form, `scanPhasePlans(...).completed`, on one
105
+ * statement — the call's own argument list is walked with a plain
106
+ * paren-depth counter (not a `[^)]*` regex), so a nested-paren
107
+ * argument (`scanPhasePlans(path.join(phasesDir, dir))`, the
108
+ * majority real shape in this tree) is still matched correctly;
109
+ * - the destructured form, `const { completed } = scanPhasePlans(…)`
110
+ * or the renamed-alias form `const { completed: isDone } =
111
+ * scanPhasePlans(…)`, on one statement;
112
+ * - the INDIRECT form — `const scan = scanPhasePlans(…)` binding the
113
+ * whole result to a variable, with `.completed` read off that same
114
+ * variable ANYWHERE else in the SAME named function, including on a
115
+ * different line — no bounded window (the same Phase-5 "bounded
116
+ * window missed 7 of 14 copies" lesson shape (b) already learned;
117
+ * `cmdStateSync`'s own real shape bound the result to a
118
+ * destructured `{ planCount: plans, summaryCount: summaries }`
119
+ * rather than a whole-object variable, so this indirect form is
120
+ * precautionary breadth for a shape not yet observed in this tree,
121
+ * not a shape already caught in the wild).
122
+ *
123
+ * DETECTION IS FUNCTION-SCOPED, NOT A BOUNDED LINE-WINDOW, for the
124
+ * *discovery* co-occurrence in each shape (shape (a)'s if/assignment pairing
125
+ * and shape (b)'s ternary-clause pairing are each ONE conditional's own two
126
+ * halves — the same narrow, unavoidable pairing `LADDER_WINDOW_LINES` bounds
127
+ * in the sibling state-field guard, not a re-derivation-hiding Goodhart
128
+ * target). Phase 5's first guard used a bounded line window between a
129
+ * ladder and its consuming call and MISSED 7 of 14 live copies inside the
130
+ * very function it scanned — that lesson is why shape (b)'s outer
131
+ * count-gate/gated-call pairing has NO line-distance bound at all: both
132
+ * signals need only appear somewhere in the SAME named function.
133
+ *
134
+ * COMMENT-AWARE. Phase 3's first `lint-phase-enumeration-drift.cjs` flagged
135
+ * JSDoc/inline comments that merely DOCUMENTED the derivation, not code that
136
+ * re-derives it (ADR-3180 Amendment 3). This guard reuses the SAME
137
+ * comment/string-stripping tokenizer `lint-state-field-drift.cjs` proved
138
+ * (`scanCode` below): comments and string/template literal CONTENTS are
139
+ * blanked before any detection regex runs, cross-line-aware for block
140
+ * comments and multi-line template literals.
141
+ *
142
+ * FUNCTION-SCOPED EXEMPTIONS ONLY, NEVER A WHOLE-FILE ALLOWLIST (Decision
143
+ * 4(a)/(d)): a whole-file exemption on the owner is precisely how
144
+ * `getMilestoneInfo` stayed invisible to an earlier guard (Decision 4(d)).
145
+ * `FUNCTION_SCOPED_EXEMPTIONS` is a `Map<relPath, Set<functionName>>`, kept
146
+ * EMPTY here because the owner (`src/verification.cts` · `isPhaseComplete`)
147
+ * does not exist yet — this comment, not a populated map, is what the
148
+ * implementation step (Phase 4's migration PR) replaces.
149
+ *
150
+ * Every regex below is small, bounded, and has no nested/overlapping
151
+ * quantifiers — the same ReDoS discipline `npm run lint:ci`'s CodeQL
152
+ * js/redos query verifies over every sibling drift guard.
153
+ *
154
+ * The tree-walk / root-confinement / sanitizer machinery is SHARED with the
155
+ * sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision
156
+ * 4).
157
+ */
158
+
159
+ const path = require('node:path');
160
+ const driftScan = require('./lib/drift-scan.cjs');
161
+ const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
162
+ const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
163
+
164
+ // ─── SHARED TOKENIZER + FUNCTION ATTRIBUTION (mirrors lint-state-field-drift.cjs) ──
165
+ //
166
+ // Two parallel per-line views from ONE single-pass, escape-aware character
167
+ // scan (not a regex — nothing for a backtracking engine to explore):
168
+ // - `detect[i]`: comments stripped, string/template CONTENTS kept verbatim
169
+ // (this is what every detection regex below runs against).
170
+ // - `braces[i]`: comments AND string/template CONTENTS stripped, used only
171
+ // for brace-depth counting, so a brace inside a string/comment never
172
+ // perturbs the depth count.
173
+ // `inBlockComment` / `inTemplate` are threaded ACROSS lines. Regex literals
174
+ // are not specially recognised — same documented, narrow, known limitation
175
+ // as the sibling guards (harmless for every regex literal actually present
176
+ // in this repo's completion-predicate call sites today, each balanced on
177
+ // its own line).
178
+ function scanCode(lines) {
179
+ const detect = new Array(lines.length);
180
+ const braces = new Array(lines.length);
181
+ let inBlockComment = false;
182
+ let inTemplate = false;
183
+ for (let li = 0; li < lines.length; li++) {
184
+ const line = lines[li];
185
+ let outDetect = '';
186
+ let outBraces = '';
187
+ let i = 0;
188
+ if (inTemplate) {
189
+ const start = i;
190
+ while (i < line.length) {
191
+ if (line[i] === '\\') {
192
+ i += 2;
193
+ continue;
194
+ }
195
+ if (line[i] === '`') {
196
+ i++;
197
+ inTemplate = false;
198
+ break;
199
+ }
200
+ i++;
201
+ }
202
+ outDetect += line.slice(start, i);
203
+ if (inTemplate) {
204
+ detect[li] = outDetect;
205
+ braces[li] = '';
206
+ continue;
207
+ }
208
+ }
209
+ while (i < line.length) {
210
+ if (inBlockComment) {
211
+ const close = line.indexOf('*/', i);
212
+ if (close === -1) {
213
+ i = line.length;
214
+ break;
215
+ }
216
+ i = close + 2;
217
+ inBlockComment = false;
218
+ continue;
219
+ }
220
+ const ch = line[i];
221
+ if (ch === '/' && line[i + 1] === '/') {
222
+ i = line.length;
223
+ break;
224
+ }
225
+ if (ch === '/' && line[i + 1] === '*') {
226
+ inBlockComment = true;
227
+ i += 2;
228
+ continue;
229
+ }
230
+ if (ch === "'" || ch === '"') {
231
+ const quote = ch;
232
+ const start = i;
233
+ let j = i + 1;
234
+ while (j < line.length) {
235
+ if (line[j] === '\\') {
236
+ j += 2;
237
+ continue;
238
+ }
239
+ if (line[j] === quote) {
240
+ j++;
241
+ break;
242
+ }
243
+ j++;
244
+ }
245
+ outDetect += line.slice(start, j);
246
+ i = j;
247
+ continue;
248
+ }
249
+ if (ch === '`') {
250
+ const start = i;
251
+ let j = i + 1;
252
+ let closed = false;
253
+ while (j < line.length) {
254
+ if (line[j] === '\\') {
255
+ j += 2;
256
+ continue;
257
+ }
258
+ if (line[j] === '`') {
259
+ j++;
260
+ closed = true;
261
+ break;
262
+ }
263
+ j++;
264
+ }
265
+ if (!closed) {
266
+ outDetect += line.slice(start);
267
+ inTemplate = true;
268
+ i = line.length;
269
+ break;
270
+ }
271
+ outDetect += line.slice(start, j);
272
+ i = j;
273
+ continue;
274
+ }
275
+ outDetect += ch;
276
+ outBraces += ch;
277
+ i++;
278
+ }
279
+ detect[li] = outDetect;
280
+ braces[li] = outBraces;
281
+ }
282
+ return { detect, braces };
283
+ }
284
+
285
+ // Named function scope openers (mirrors lint-state-field-drift.cjs exactly):
286
+ // - `function NAME(...) {` — top-level OR nested, any indentation, an
287
+ // optional leading `export ` tolerated by `\b` alone.
288
+ const FUNCTION_DECL_RE = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/;
289
+ // - `const NAME = (...): ReturnType => {` — block-bodied arrow assigned to
290
+ // a const (an expression-bodied arrow `=> ({...})` never opens a new
291
+ // function frame; its `{` is an object literal, still counted toward
292
+ // brace depth, but attributes no name).
293
+ const ARROW_CONST_RE = /\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*\([^)]*\)\s*(?::\s*[^=]+)?=>\s*\{/;
294
+
295
+ /**
296
+ * Pure: walk `lines` once, maintaining a brace-depth stack of open named
297
+ * function frames (deferring a multi-line `function NAME(` signature until
298
+ * the line whose brace count actually increases — see
299
+ * `lint-state-field-drift.cjs`'s `buildFunctionInfo` header for the full
300
+ * rationale this mirrors verbatim). Returns `{ innermostAt, detect }`:
301
+ * `innermostAt[i]` is the name of the innermost named function open at line
302
+ * `i` (or `null` at module scope), `detect[i]` is the comment/string-
303
+ * preserving-but-stripped-of-comments view every detector regex runs
304
+ * against.
305
+ */
306
+ function buildFunctionInfo(lines) {
307
+ const { detect, braces } = scanCode(lines);
308
+ const innermostAt = new Array(lines.length).fill(null);
309
+ const stack = []; // { name, openDepth }
310
+ let depth = 0;
311
+ let pendingDeclName = null;
312
+ for (let i = 0; i < lines.length; i++) {
313
+ const detectCode = detect[i];
314
+ const braceCode = braces[i];
315
+
316
+ let immediateName = null;
317
+ if (detectCode.trim()) {
318
+ const arrowMatch = ARROW_CONST_RE.exec(detectCode);
319
+ if (arrowMatch) {
320
+ immediateName = arrowMatch[1];
321
+ } else {
322
+ const declMatch = FUNCTION_DECL_RE.exec(detectCode);
323
+ if (declMatch) pendingDeclName = declMatch[1];
324
+ }
325
+ }
326
+
327
+ const opens = (braceCode.match(/\{/g) || []).length;
328
+ const closes = (braceCode.match(/\}/g) || []).length;
329
+ depth += opens - closes;
330
+
331
+ if (immediateName) stack.push({ name: immediateName, openDepth: depth });
332
+
333
+ if (pendingDeclName) {
334
+ if (opens > 0) {
335
+ stack.push({ name: pendingDeclName, openDepth: depth });
336
+ pendingDeclName = null;
337
+ } else if (detectCode.includes(';')) {
338
+ pendingDeclName = null;
339
+ }
340
+ }
341
+
342
+ while (stack.length > 0 && depth < stack[stack.length - 1].openDepth) stack.pop();
343
+
344
+ innermostAt[i] = stack.length > 0 ? stack[stack.length - 1].name : null;
345
+ }
346
+ return { innermostAt, detect };
347
+ }
348
+
349
+ /**
350
+ * §7.4/#3186 review finding 4(a): the BLOCK form of shape (b)'s gate — `if
351
+ * (planCount > 0) { … readVerificationStatus(…) … }` — is textually
352
+ * indistinguishable from an unrelated `if` block by a same-line regex; the
353
+ * prior ternary-only `GATED_VERIFICATION_READ_RE` produced zero hits on it.
354
+ * Deliberately narrower than a generic "is this line nested at all" check
355
+ * (which would false-positive on every unrelated wrapper — a `try` block, a
356
+ * `withPlanningLock(cwd, () => { … })` callback, a `for` loop — none of
357
+ * which are a plan-count GATE): only lines inside the BODY of an `if (...)`
358
+ * whose OWN condition matches `COUNT_GATE_RE` are marked. Line-granularity
359
+ * brace bookkeeping (mirrors `buildFunctionInfo`'s own style), not a
360
+ * per-character brace matcher — a multi-line `if` condition whose `{` lands
361
+ * on a later line than `extractIfCondition`'s reported `endLine` is a known,
362
+ * disclosed limitation (real count-gates in this tree are one-line
363
+ * conditions; see the module header).
364
+ */
365
+ function findIfCountGateBlockLines(lines) {
366
+ const { detect, braces } = scanCode(lines);
367
+ const gated = new Array(lines.length).fill(false);
368
+ const stack = []; // { openDepth } for open if-blocks whose condition is a count-gate
369
+ let depth = 0;
370
+ for (let i = 0; i < lines.length; i++) {
371
+ const detectCode = detect[i];
372
+ const braceCode = braces[i];
373
+
374
+ let isCountGateIfHeader = false;
375
+ if (detectCode.trim()) {
376
+ const ifMatch = IF_OPEN_RE.exec(detectCode);
377
+ if (ifMatch) {
378
+ const startCol = ifMatch.index + ifMatch[0].length;
379
+ const condition = extractIfCondition(detect, i, startCol);
380
+ if (condition && COUNT_GATE_RE.test(condition.text)) isCountGateIfHeader = true;
381
+ }
382
+ }
383
+
384
+ const opens = (braceCode.match(/\{/g) || []).length;
385
+ const closes = (braceCode.match(/\}/g) || []).length;
386
+ depth += opens - closes;
387
+
388
+ // A line is "inside" a count-gate if-block when either a PRIOR line
389
+ // already opened one and it has not yet closed, or THIS line's own `if`
390
+ // header both matches the gate and opens its body on the same line
391
+ // (`if (planCount > 0) { … }` — the exact shape in the finding).
392
+ gated[i] = stack.length > 0 || (isCountGateIfHeader && opens > closes);
393
+
394
+ if (isCountGateIfHeader && opens > closes) stack.push({ openDepth: depth });
395
+
396
+ while (stack.length > 0 && depth < stack[stack.length - 1].openDepth) stack.pop();
397
+ }
398
+ return gated;
399
+ }
400
+
401
+ // ─── SHAPE (a): CHECKBOX-DERIVED COMPLETION ────────────────────────────────
402
+ //
403
+ // The `if` half of the pairing: a condition referencing a roadmap/checkbox-
404
+ // derived "complete" boolean (`roadmapComplete`, `roadmap_complete`, any
405
+ // `\w*roadmap\w*complete\w*` spelling — case-insensitive, both real sites in
406
+ // this tree use exactly `roadmapComplete`) AND testing some OTHER status
407
+ // variable against the literal `!== 'complete'` (single or double quotes).
408
+ const IF_OPEN_RE = /\bif\s*\(/;
409
+ const ROADMAP_COMPLETE_IDENT_RE = /\broadmap\w*complete\w*\b/i;
410
+ const STATUS_NEQ_COMPLETE_RE = /\b([A-Za-z_$][\w$]*)\s*!==\s*['"]complete['"]/;
411
+
412
+ // The consequent half: an assignment of the SAME status variable to the
413
+ // literal `'complete'`. The negative lookbehind excludes `!==`/`<=`/`>=`/`==`
414
+ // (each of which also contains a bare `=` immediately before a quote) so
415
+ // this never re-matches the `if` line's own `!== 'complete'` clause — a
416
+ // single bounded character class, not a nested quantifier.
417
+ const ASSIGN_COMPLETE_RE = /(?<![!<>=])=\s*['"]complete['"]/;
418
+
419
+ // How many lines the consequent assignment may trail its own `if` line by.
420
+ // This bounds ONE conditional's own two halves (condition, then its direct
421
+ // consequent statement) — the same narrow role `LADDER_WINDOW_LINES` plays
422
+ // in the sibling state-field guard, not the function-scoped, unbounded
423
+ // co-occurrence Decision 4(a)'s Goodhart lesson targets for shape (b) below.
424
+ const CHECKBOX_OVERRIDE_WINDOW_LINES = 4;
425
+
426
+ // The `if (...)` condition in both real sites nests a SECOND, unrelated
427
+ // parenthesised group (`(completion.phase_complete || planCount === 0)`), so
428
+ // a single-line, no-nested-parens regex over the whole condition
429
+ // systematically MISSES the second site. Extracted by plain paren-depth
430
+ // counting instead — a linear character walk, not a regex, so there is
431
+ // nothing for a backtracking engine to explore regardless of nesting depth.
432
+ // Bounded to IF_CONDITION_MAX_LINES so a pathologically unterminated `if (`
433
+ // cannot walk the whole file.
434
+ const IF_CONDITION_MAX_LINES = 10;
435
+
436
+ function extractIfCondition(detectLines, startLine, startCol) {
437
+ let depth = 1; // the `(` at startCol already opened the condition
438
+ let text = '';
439
+ const endLineLimit = Math.min(detectLines.length, startLine + IF_CONDITION_MAX_LINES);
440
+ for (let li = startLine; li < endLineLimit; li++) {
441
+ const line = detectLines[li];
442
+ const from = li === startLine ? startCol : 0;
443
+ for (let ci = from; ci < line.length; ci++) {
444
+ const ch = line[ci];
445
+ if (ch === '(') depth++;
446
+ else if (ch === ')') {
447
+ depth--;
448
+ if (depth === 0) return { text, endLine: li };
449
+ }
450
+ text += ch;
451
+ }
452
+ text += '\n';
453
+ }
454
+ return null; // unterminated within the bound — treated as no match
455
+ }
456
+
457
+ function findChecklistOverrideDrift(text, relPath, exemptFunctions) {
458
+ const out = [];
459
+ const lines = text.split('\n');
460
+ const { innermostAt, detect } = buildFunctionInfo(lines);
461
+ for (let i = 0; i < lines.length; i++) {
462
+ const detectCode = detect[i];
463
+ if (!detectCode.trim()) continue;
464
+ const ifMatch = IF_OPEN_RE.exec(detectCode);
465
+ if (!ifMatch) continue;
466
+ const startCol = ifMatch.index + ifMatch[0].length;
467
+ const condition = extractIfCondition(detect, i, startCol);
468
+ if (!condition) continue;
469
+ if (!ROADMAP_COMPLETE_IDENT_RE.test(condition.text)) continue;
470
+ const neqMatch = STATUS_NEQ_COMPLETE_RE.exec(condition.text);
471
+ if (!neqMatch) continue;
472
+ const varName = neqMatch[1];
473
+ const limit = Math.min(lines.length, condition.endLine + 1 + CHECKBOX_OVERRIDE_WINDOW_LINES);
474
+ for (let j = condition.endLine + 1; j < limit; j++) {
475
+ const assignCode = detect[j];
476
+ if (!assignCode.trim()) continue;
477
+ if (!assignCode.includes(varName)) continue;
478
+ if (!ASSIGN_COMPLETE_RE.test(assignCode)) continue;
479
+ const fn = innermostAt[j] || innermostAt[i];
480
+ if (fn && exemptFunctions && exemptFunctions.has(fn)) break;
481
+ out.push({ line: j + 1, found: lines[j].trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'a', fn: fn || null });
482
+ break;
483
+ }
484
+ }
485
+ return out;
486
+ }
487
+
488
+ // ─── SHAPE (b): PLAN-COUNT PRECONDITION GATING A VERIFICATION READ ─────────
489
+ //
490
+ // A "count > 0"-shaped gate — any identifier containing `count`
491
+ // (case-insensitive) compared `> 0`. Function-scoped presence only (no
492
+ // window): §7.4 names this exact shape as `planCount > 0`, but the
493
+ // identifier is matched generically so a differently-named count (or a
494
+ // future `isPhaseComplete` re-implementation reusing the same gate under a
495
+ // new name) is still caught.
496
+ const COUNT_GATE_RE = /\b[A-Za-z_$][\w$]*count\b\s*>\s*0\b/i;
497
+
498
+ // The call this derivation's owner (`readVerificationStatus`, wrapped by the
499
+ // not-yet-existing `isPhaseComplete`) must run UNCONDITIONALLY per §7.4. A
500
+ // line where `readVerificationStatus(` is reached via a ternary — a `?`
501
+ // appearing anywhere earlier on the SAME line — is a GATED call, not an
502
+ // unconditional one. `[^\n]*` is bounded by the line itself (no backtracking
503
+ // blow-up: a single non-newline character class followed by one literal).
504
+ const VERIFICATION_READ_CALL_RE = /\breadVerificationStatus\(/;
505
+ const GATED_VERIFICATION_READ_RE = /\?[^\n]*\breadVerificationStatus\(/;
506
+
507
+ function findGatedVerificationReadDrift(text, relPath, exemptFunctions) {
508
+ const out = [];
509
+ const lines = text.split('\n');
510
+ const { innermostAt, detect } = buildFunctionInfo(lines);
511
+ const ifCountGateBlockLines = findIfCountGateBlockLines(lines);
512
+
513
+ const countGateFns = new Set();
514
+ for (let i = 0; i < lines.length; i++) {
515
+ const detectCode = detect[i];
516
+ if (!detectCode.trim()) continue;
517
+ if (COUNT_GATE_RE.test(detectCode)) {
518
+ const fn = innermostAt[i];
519
+ if (fn) countGateFns.add(fn);
520
+ }
521
+ }
522
+
523
+ for (let i = 0; i < lines.length; i++) {
524
+ const detectCode = detect[i];
525
+ if (!detectCode.trim()) continue;
526
+ if (!VERIFICATION_READ_CALL_RE.test(detectCode)) continue;
527
+ // #3186 review finding 4(a): "gated" is EITHER a same-line ternary `?`
528
+ // before the call, OR the call sitting inside the BODY of an `if (...)`
529
+ // block whose own condition is a count-gate (`findIfCountGateBlockLines`)
530
+ // — the latter catches the block form (`if (planCount > 0) { …
531
+ // readVerificationStatus(…) … }`), which the ternary-only regex produced
532
+ // zero hits on.
533
+ const ternaryGated = GATED_VERIFICATION_READ_RE.test(detectCode);
534
+ const blockGated = ifCountGateBlockLines[i];
535
+ if (!ternaryGated && !blockGated) continue;
536
+ const fn = innermostAt[i];
537
+ if (!fn || !countGateFns.has(fn)) continue;
538
+ if (exemptFunctions && exemptFunctions.has(fn)) continue;
539
+ out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'b', fn });
540
+ }
541
+ return out;
542
+ }
543
+
544
+ // ─── SHAPE (c): LOCAL RE-IMPLEMENTATION OF "COMPLETE" FROM COUNTS ──────────
545
+ //
546
+ // `summaryCount >= planCount` (either identifier order, either comparison
547
+ // direction) — the single textual signature `buildPhaseCompletionProjection`,
548
+ // `scanPhasePlans`, `cmdRoadmapAnalyze`, `cmdRoadmapUpdatePlanProgress`, and
549
+ // `buildWorkstreamInventory` each independently hand-roll. Case-insensitive
550
+ // so `SummaryCount`/`summary_count` spellings are still caught; both operand
551
+ // orders are covered by two small, non-overlapping alternatives.
552
+ const SUMMARY_GE_PLAN_RE = /\bsummar\w*count\w*\s*>=\s*\w*plan\w*count\w*/i;
553
+ const PLAN_LE_SUMMARY_RE = /\bplan\w*count\w*\s*<=\s*\w*summar\w*count\w*/i;
554
+
555
+ // §7.4/#3186 review finding 4(b): the literal `>=`/`<=` regexes above missed
556
+ // the algebraic restatement `summaryCount - planCount >= 0` (and its mirror,
557
+ // `planCount - summaryCount <= 0`) — same comparison, no literal `>=`/`<=`
558
+ // between the two count identifiers. Widened to cover exactly these two
559
+ // zero-compared-difference shapes; each is a single bounded alternative, no
560
+ // nested/overlapping quantifiers.
561
+ //
562
+ // KNOWN, DISCLOSED LIMIT (not claimed covered): arbitrary further algebraic
563
+ // restatements — `!(planCount > summaryCount)`, a difference stored in an
564
+ // intermediate variable before the comparison, `Math.max(0, planCount -
565
+ // summaryCount) === 0`, etc. — are NOT reliably detectable by a bounded,
566
+ // non-backtracking regex and are not attempted here. This is a genuine gap,
567
+ // not swept under "et cetera": the header above disclosed it as such rather
568
+ // than claiming a wider net than the regexes actually cast.
569
+ const SUMMARY_MINUS_PLAN_GE_ZERO_RE = /\bsummar\w*count\w*\s*-\s*\w*plan\w*count\w*\s*>=\s*0\b/i;
570
+ const PLAN_MINUS_SUMMARY_LE_ZERO_RE = /\bplan\w*count\w*\s*-\s*\w*summar\w*count\w*\s*<=\s*0\b/i;
571
+
572
+ function findLocalCompletionCountDerivationDrift(text, relPath, exemptFunctions) {
573
+ const out = [];
574
+ const lines = text.split('\n');
575
+ const { innermostAt, detect } = buildFunctionInfo(lines);
576
+ for (let i = 0; i < lines.length; i++) {
577
+ const detectCode = detect[i];
578
+ if (!detectCode.trim()) continue;
579
+ if (
580
+ !SUMMARY_GE_PLAN_RE.test(detectCode)
581
+ && !PLAN_LE_SUMMARY_RE.test(detectCode)
582
+ && !SUMMARY_MINUS_PLAN_GE_ZERO_RE.test(detectCode)
583
+ && !PLAN_MINUS_SUMMARY_LE_ZERO_RE.test(detectCode)
584
+ ) continue;
585
+ const fn = innermostAt[i];
586
+ if (fn && exemptFunctions && exemptFunctions.has(fn)) continue;
587
+ out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'c', fn: fn || null });
588
+ }
589
+ return out;
590
+ }
591
+
592
+ // ─── SHAPE (d): scanPhasePlans(...).completed READ AS A COMPLETION VERDICT ─
593
+ //
594
+ // See the module header's shape (d) entry for the full rationale and the
595
+ // three textual forms detected below. `FUNCTION_SCOPED_EXEMPTIONS` is
596
+ // SHARED with shapes (a)/(b)/(c) — the same per-file, per-function map, so a
597
+ // function already exempted for one shape (e.g. `scanPhasePlans` itself,
598
+ // which legitimately builds the `completed` field it returns) is exempted
599
+ // for shape (d) too, and a function newly exempted for shape (d) must carry
600
+ // its own written reason in that map's comment exactly like the others.
601
+ const SCAN_CALL_TOKEN = 'scanPhasePlans(';
602
+ const DOT_COMPLETED_RE = /^\.completed\b/;
603
+ const SCAN_ASSIGN_VAR_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*scanPhasePlans\(/;
604
+ // Bounded: `[^{}]*` is a single, non-nested, non-overlapping quantifier — no
605
+ // backtracking blow-up regardless of destructure-pattern length. Real
606
+ // destructuring assignments in this tree are single-line (the same
607
+ // assumption `SCAN_ASSIGN_VAR_RE` and every sibling shape's regexes make).
608
+ const DESTRUCTURE_ASSIGN_RE = /\{([^{}]*)\}\s*=\s*scanPhasePlans\(/;
609
+ const COMPLETED_KEY_RE = /\bcompleted\b/;
610
+
611
+ function isWordChar(ch) {
612
+ return !!ch && /[A-Za-z0-9_$]/.test(ch);
613
+ }
614
+
615
+ // Plain paren-depth counter over a SINGLE line (not a regex — nothing for a
616
+ // backtracking engine to explore), so a nested-paren call argument (e.g.
617
+ // `scanPhasePlans(path.join(phasesDir, dir))`, the majority real shape in
618
+ // this tree) still resolves to the call's own true closing `)`. Confined to
619
+ // one line: every real `scanPhasePlans(` call site in this tree closes on
620
+ // the line it opens on (the same assumption the rest of this file's
621
+ // detectors make about this specific call).
622
+ function findCallEndOnLine(line, afterOpenParenIdx) {
623
+ let depth = 1;
624
+ for (let i = afterOpenParenIdx; i < line.length; i++) {
625
+ const ch = line[i];
626
+ if (ch === '(') depth++;
627
+ else if (ch === ')') {
628
+ depth--;
629
+ if (depth === 0) return i + 1;
630
+ }
631
+ }
632
+ return -1;
633
+ }
634
+
635
+ function findScanPhasePlansCompletedReadDrift(text, relPath, exemptFunctions) {
636
+ const out = [];
637
+ const lines = text.split('\n');
638
+ const { innermostAt, detect } = buildFunctionInfo(lines);
639
+ const isExemptAt = (i) => {
640
+ const fn = innermostAt[i];
641
+ return !!(fn && exemptFunctions && exemptFunctions.has(fn));
642
+ };
643
+
644
+ // fnKey: the innermost function name at the ASSIGNMENT site, or this
645
+ // sentinel for module-scope assignments (never collides with a real
646
+ // identifier — function names cannot start with U+0000).
647
+ const FN_KEY_MODULE = 'module';
648
+ const varsByFn = new Map(); // fnKey -> Set<varName> bound directly (non-destructured) to a scanPhasePlans( result
649
+
650
+ for (let i = 0; i < lines.length; i++) {
651
+ const detectCode = detect[i];
652
+ if (!detectCode.trim()) continue;
653
+
654
+ // Direct chained form: scanPhasePlans(...).completed — every occurrence
655
+ // on the line, paren-matched (not a naive `[^)]*`, which would stop at
656
+ // the FIRST `)`, i.e. the inner `path.join(...)`'s own closing paren).
657
+ let searchFrom = 0;
658
+ for (;;) {
659
+ const idx = detectCode.indexOf(SCAN_CALL_TOKEN, searchFrom);
660
+ if (idx === -1) break;
661
+ if (isWordChar(detectCode[idx - 1])) {
662
+ searchFrom = idx + 1;
663
+ continue;
664
+ }
665
+ const callEnd = findCallEndOnLine(detectCode, idx + SCAN_CALL_TOKEN.length);
666
+ if (callEnd === -1) {
667
+ searchFrom = idx + 1;
668
+ continue;
669
+ }
670
+ if (DOT_COMPLETED_RE.test(detectCode.slice(callEnd)) && !isExemptAt(i)) {
671
+ out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'd', fn: innermostAt[i] || null });
672
+ }
673
+ searchFrom = callEnd;
674
+ }
675
+
676
+ // Destructured form: const { completed[, ...] } = scanPhasePlans(...)
677
+ // (bare key or a `completed: alias` rename).
678
+ const destructureMatch = DESTRUCTURE_ASSIGN_RE.exec(detectCode);
679
+ if (destructureMatch && COMPLETED_KEY_RE.test(destructureMatch[1]) && !isExemptAt(i)) {
680
+ out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'd', fn: innermostAt[i] || null });
681
+ }
682
+
683
+ // Indirect form, pass 1: const NAME = scanPhasePlans(...) — record the
684
+ // binding; the read (possibly on a LATER line) is matched in pass 2
685
+ // below, function-scoped with no line window.
686
+ const assignMatch = SCAN_ASSIGN_VAR_RE.exec(detectCode);
687
+ if (assignMatch) {
688
+ const fnKey = innermostAt[i] || FN_KEY_MODULE;
689
+ if (!varsByFn.has(fnKey)) varsByFn.set(fnKey, new Set());
690
+ varsByFn.get(fnKey).add(assignMatch[1]);
691
+ }
692
+ }
693
+
694
+ // Indirect form, pass 2: NAME.completed anywhere in the SAME function that
695
+ // bound NAME to a scanPhasePlans( result — Decision 4(a)'s Goodhart
696
+ // lesson again: no bounded distance between the binding and the read.
697
+ for (const [fnKey, varNames] of varsByFn) {
698
+ if (fnKey !== FN_KEY_MODULE && exemptFunctions && exemptFunctions.has(fnKey)) continue;
699
+ for (const varName of varNames) {
700
+ const escaped = escapeRegex(varName);
701
+ const readRe = new RegExp(`\\b${escaped}\\.completed\\b`);
702
+ for (let i = 0; i < lines.length; i++) {
703
+ const fnHere = innermostAt[i] || FN_KEY_MODULE;
704
+ if (fnHere !== fnKey) continue;
705
+ if (!detect[i].trim()) continue;
706
+ if (readRe.test(detect[i])) {
707
+ out.push({
708
+ line: i + 1,
709
+ found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN),
710
+ shape: 'd',
711
+ fn: fnKey === FN_KEY_MODULE ? null : fnKey,
712
+ });
713
+ }
714
+ }
715
+ }
716
+ }
717
+
718
+ return out;
719
+ }
720
+
721
+ function findCompletionPredicateDrift(text, relPath) {
722
+ const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
723
+ return [
724
+ ...findChecklistOverrideDrift(text, relPath, exemptFunctions),
725
+ ...findGatedVerificationReadDrift(text, relPath, exemptFunctions),
726
+ ...findLocalCompletionCountDerivationDrift(text, relPath, exemptFunctions),
727
+ ...findScanPhasePlansCompletedReadDrift(text, relPath, exemptFunctions),
728
+ ].sort((x, y) => x.line - y.line);
729
+ }
730
+
731
+ // ─── PROMPT-LAYER PROSE/SHELL DETECTION (ADR-3180 Decision 4(d)) ───────────
732
+ //
733
+ // The SAME question — "is phase P complete?" — expressed as shell/jq inside
734
+ // workflow markdown rather than TypeScript. `gsd-core/workflows/mvp-phase.md`
735
+ // reads `.roadmap_complete` (a ROADMAP-checkbox-derived JSON field) into a
736
+ // shell variable and later OR's it directly into a `STATUS="completed"`
737
+ // decision — shape (a), independently of and in addition to
738
+ // `cmdRoadmapAnalyze`'s own checkbox override at the source of that field.
739
+ //
740
+ // Two-line pairing, mirroring the TS-side shape (a) detector: line A assigns
741
+ // a shell variable from `.roadmap_complete` (or `.roadmap_complete` accessed
742
+ // any other way jq/shell might spell it); line B, within a bounded window
743
+ // (shell scripts interleave unrelated statements more than a single `if`
744
+ // body does, so this window is wider than the TS pairing's), uses that same
745
+ // variable in a `==` test against `"true"` — the same tight, unavoidable
746
+ // two-statement pairing as its TS counterpart, not a Goodhart-vulnerable
747
+ // function-scoped sweep (a Bash script has no function-scope concept this
748
+ // guard tracks).
749
+ const PROMPT_ROADMAP_COMPLETE_ASSIGN_RE = /^([A-Za-z_][A-Za-z0-9_]*)=.*\.roadmap_complete\b/;
750
+ const PROMPT_TRUE_TEST_RE = /==\s*['"]true['"]/;
751
+ const PROMPT_OVERRIDE_WINDOW_LINES = 10;
752
+
753
+ // #3186 review finding 7(b): `relPath` is unused here — this detector has no
754
+ // per-file exemption map the way `findCompletionPredicateDrift` does (the
755
+ // prompt layer has no FUNCTION_SCOPED_EXEMPTIONS equivalent). Kept in the
756
+ // signature (underscore-prefixed) for parity with its sibling `find*Drift`
757
+ // detectors' `(text, relPath, …)` shape and with `scanRepo`'s uniform
758
+ // `finder(text, rel)` call, rather than diverging the call convention.
759
+ function findPromptCompletionDrift(text, _relPath) {
760
+ const out = [];
761
+ const lines = text.split('\n');
762
+ for (let i = 0; i < lines.length; i++) {
763
+ const assignMatch = PROMPT_ROADMAP_COMPLETE_ASSIGN_RE.exec(lines[i]);
764
+ if (!assignMatch) continue;
765
+ const varName = assignMatch[1];
766
+ const limit = Math.min(lines.length, i + 1 + PROMPT_OVERRIDE_WINDOW_LINES);
767
+ for (let j = i + 1; j < limit; j++) {
768
+ const line = lines[j];
769
+ if (!line.includes(`$${varName}`)) continue;
770
+ if (!PROMPT_TRUE_TEST_RE.test(line)) continue;
771
+ out.push({ line: j + 1, found: line.trim().slice(0, MAX_REGEX_LITERAL_LEN), shape: 'a' });
772
+ break;
773
+ }
774
+ }
775
+ return out;
776
+ }
777
+
778
+ // Authored TypeScript source AND the prompt layer (ADR-3180 Decision 4(d)):
779
+ // `src/` alone is itself the forbidden allowlist Decision 4(d) names.
780
+ const SCAN_DIRS = ['src', 'gsd-core/workflows', 'commands', 'agents', 'skills'];
781
+ const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.md']);
782
+
783
+ // The designated owner (issue #3186, ADR-3180 §7.4) — does NOT exist yet.
784
+ const OWNER_FILE = path.join('src', 'verification.cts');
785
+
786
+ // Per ADR-3180 Decision 4(a)/(d): function-scoped, NEVER a whole-file
787
+ // allowlist. Populated by Phase 4's implementation step (issue #3186):
788
+ //
789
+ // - `isPhaseComplete` (OWNER_FILE, src/verification.cts) — the canonical
790
+ // owner itself. It reads `verification.status === 'passed'` (shape (c)'s
791
+ // textual match is a false positive here: the owner's OWN comparison is
792
+ // not a re-derivation of itself) and its readdirSync-based readability
793
+ // check for `scope: UNREADABLE` sits beside a ternary-gated read that is
794
+ // NOT a `readVerificationStatus(` call, so shapes (a)/(b) never fire here
795
+ // either — this entry exists for defence in depth and Decision 4(d)'s
796
+ // "the owner file is not exempt, only its named function is" rule.
797
+ //
798
+ // - `scanPhasePlans` (src/plan-scan.cts) — ADR-3180 §7.4 / the design's
799
+ // "0.x split": `completed: allPlanFiles.length > 0 && summaryCount >=
800
+ // planCount` answers "are all plans summarized?", NOT "is the phase
801
+ // complete?" (completion additionally requires a passing
802
+ // `*-VERIFICATION.md`). Folding it onto `isPhaseComplete` would either
803
+ // over-report completion or drag a verification read into plan-scan.cts,
804
+ // inverting the dependency direction between Phase 1's owner and this
805
+ // one (the owner consumes plan counts, never the reverse). Left as its
806
+ // own, differently-scoped answer, per design's "Rejected" list item 1.
807
+ //
808
+ // - `buildWorkstreamInventory` (src/workstream-inventory-builder.cts) — a
809
+ // PURE, I/O-free projection (see the module's own header) over
810
+ // PRE-COLLECTED `planCount`/`summaryCount`/`verificationStatus` inputs.
811
+ // `summariesMeetPlans = summaryCount >= planCount && planCount > 0`
812
+ // answers "are all plans summarized?" (combined with the caller-supplied
813
+ // verification verdict for its own `status` projection) — it cannot call
814
+ // the I/O-bound `isPhaseComplete` without breaking its "No I/O. No
815
+ // async." contract, and re-plumbing its callers to pass a pre-computed
816
+ // `complete` boolean instead of raw counts is a larger architectural
817
+ // change than this phase's declared 6 call sites. Declared deviation —
818
+ // see the Phase 4 migration PR for the full reasoning.
819
+ //
820
+ // - `buildPhaseCompletionProjection` (src/init.cts) — MIGRATED onto
821
+ // `isPhaseComplete` for `phase_complete`/`verification_status` (shape
822
+ // (b) and the unconditional-read half of shape (c) are gone), but it
823
+ // independently retains `implementation_complete = planCount > 0 &&
824
+ // summaryCount >= planCount` as a DIFFERENT, still-needed answer ("are
825
+ // plans done", not "is the phase complete") that `isPhaseComplete`'s
826
+ // locked `{ complete, verification }` return shape does not carry and
827
+ // that downstream consumers (`disk_status: 'executed'` vs `'planned'`)
828
+ // still depend on. This is a declared deviation, not named by the design
829
+ // doc's own DO-NOT-MIGRATE list (which named only `scanPhasePlans` and
830
+ // `buildWorkstreamInventory`) — flagged for orchestrator review rather
831
+ // than silently exempted.
832
+ //
833
+ // SHAPE (d) reuses this exact map (`findScanPhasePlansCompletedReadDrift`
834
+ // takes the same `exemptFunctions` set every other shape's finder does). The
835
+ // four entries above were entered for shapes (a)/(b)/(c); of them, only
836
+ // `scanPhasePlans` (src/plan-scan.cts) also legitimately reads its OWN
837
+ // `completed` field for shape (d)'s purposes (building the return value it
838
+ // itself defines — not a call-then-read of another `scanPhasePlans(`
839
+ // invocation). `isPhaseComplete`, `buildWorkstreamInventory`, and
840
+ // `buildPhaseCompletionProjection` do not read `.completed` off a
841
+ // `scanPhasePlans(` call result at all (verified by direct inspection of
842
+ // each function's body as of this guard's shape-(d) addition — they consume
843
+ // `planCount`/`summaryCount`/`summaryFiles`, never `.completed`), so their
844
+ // presence in this map exempts nothing NEW for shape (d); they are listed
845
+ // here only because the map is shared. As of this addition, a whole-repo
846
+ // scan found ZERO live shape-(d) sites needing a fresh exemption entry — the
847
+ // one real instance this shape exists to catch (`cmdStateSync`,
848
+ // src/state.cts) was fixed by routing through `isPhaseComplete` rather than
849
+ // being exempted, which is the outcome ADR-3180 §7.4 requires. If a future
850
+ // change legitimately needs a NEW shape-(d) exemption, add it here with its
851
+ // own written reason — do not silently extend an existing entry's Set.
852
+ const FUNCTION_SCOPED_EXEMPTIONS = new Map([
853
+ [OWNER_FILE, new Set(['isPhaseComplete'])],
854
+ [path.join('src', 'plan-scan.cts'), new Set(['scanPhasePlans'])],
855
+ [path.join('src', 'workstream-inventory-builder.cts'), new Set(['buildWorkstreamInventory'])],
856
+ [path.join('src', 'init.cts'), new Set(['buildPhaseCompletionProjection'])],
857
+ ]);
858
+
859
+ function scanRepo(root) {
860
+ return scanTree({
861
+ root,
862
+ scanDirs: SCAN_DIRS,
863
+ scanExt: SCAN_EXT,
864
+ onFile(rel, text) {
865
+ const finder = path.extname(rel) === '.md' ? findPromptCompletionDrift : findCompletionPredicateDrift;
866
+ return finder(text, rel).map((d) => ({ file: rel, ...d }));
867
+ },
868
+ });
869
+ }
870
+
871
+ function main() {
872
+ const root = path.join(__dirname, '..');
873
+ const violations = scanRepo(root);
874
+ if (violations.length === 0) {
875
+ process.stdout.write('ok completion-predicate-drift: no unsanctioned phase-completion re-derivations found\n');
876
+ return;
877
+ }
878
+ process.stderr.write('completion-predicate-drift: independent re-derivation(s) of the phase-completion\n');
879
+ process.stderr.write('predicate ("is phase P complete?") found. Route these call sites through\n');
880
+ process.stderr.write('src/verification.cts `isPhaseComplete` (issue #3186, ADR-3180 §7.4) instead of\n');
881
+ process.stderr.write('re-deriving it locally:\n');
882
+ for (const d of violations) {
883
+ const shapeLabel = d.shape ? `[shape ${d.shape}]` : '';
884
+ const fnLabel = d.fn ? ` (in ${sanitizeForReport(d.fn)})` : '';
885
+ process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${shapeLabel}${fnLabel} ${sanitizeForReport(d.found)}\n`);
886
+ }
887
+ process.exitCode = 1;
888
+ }
889
+
890
+ if (require.main === module) main();
891
+
892
+ module.exports = {
893
+ findCompletionPredicateDrift,
894
+ findChecklistOverrideDrift,
895
+ findGatedVerificationReadDrift,
896
+ findLocalCompletionCountDerivationDrift,
897
+ findScanPhasePlansCompletedReadDrift,
898
+ findPromptCompletionDrift,
899
+ buildFunctionInfo,
900
+ findIfCountGateBlockLines,
901
+ scanCode,
902
+ scanRepo,
903
+ IF_OPEN_RE,
904
+ ROADMAP_COMPLETE_IDENT_RE,
905
+ STATUS_NEQ_COMPLETE_RE,
906
+ ASSIGN_COMPLETE_RE,
907
+ CHECKBOX_OVERRIDE_WINDOW_LINES,
908
+ IF_CONDITION_MAX_LINES,
909
+ extractIfCondition,
910
+ COUNT_GATE_RE,
911
+ VERIFICATION_READ_CALL_RE,
912
+ GATED_VERIFICATION_READ_RE,
913
+ SUMMARY_GE_PLAN_RE,
914
+ PLAN_LE_SUMMARY_RE,
915
+ SUMMARY_MINUS_PLAN_GE_ZERO_RE,
916
+ PLAN_MINUS_SUMMARY_LE_ZERO_RE,
917
+ SCAN_CALL_TOKEN,
918
+ DOT_COMPLETED_RE,
919
+ SCAN_ASSIGN_VAR_RE,
920
+ DESTRUCTURE_ASSIGN_RE,
921
+ COMPLETED_KEY_RE,
922
+ findCallEndOnLine,
923
+ PROMPT_ROADMAP_COMPLETE_ASSIGN_RE,
924
+ PROMPT_TRUE_TEST_RE,
925
+ PROMPT_OVERRIDE_WINDOW_LINES,
926
+ FUNCTION_DECL_RE,
927
+ ARROW_CONST_RE,
928
+ OWNER_FILE,
929
+ FUNCTION_SCOPED_EXEMPTIONS,
930
+ SCAN_DIRS,
931
+ SCAN_EXT,
932
+ MAX_REGEX_LITERAL_LEN,
933
+ };