@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,303 @@
1
+ "use strict";
2
+ /**
3
+ * Plan Dependency Graph — shared halt-propagation over a plan's depends_on DAG (#2830).
4
+ *
5
+ * Two independent "which plans are incomplete" readers exist in this codebase:
6
+ * phase.cts's wave-grouping (`cmdPhasePlanIndex`) and phase-locator.cts's
7
+ * phase-location primitive (`searchPhaseInDir`, consumed by ~50 symbols across
8
+ * five command routers). Before #2830, only the former parsed `depends_on` —
9
+ * and even it used the DAG only for topological wave assignment, never to
10
+ * propagate a halted plan's block onto its dependents. The latter never parsed
11
+ * `depends_on` at all; it derived completion from summary-file presence only.
12
+ * A plan that reaches a designed stop still writes a SUMMARY (recording the
13
+ * halt in prose only — no prior structured status existed for it), so both
14
+ * readers saw it as ordinary "complete" and reported its dependents as
15
+ * ordinary incomplete work, available to spawn against.
16
+ *
17
+ * This module is the SINGLE topological-order + halt-propagation engine both
18
+ * readers call, so the two can never re-diverge on this rule again. Each
19
+ * caller resolves its own raw `depends_on` tokens to canonical plan ids
20
+ * (case-fold + `extractCanonicalPlanId` fallback — the same resolution
21
+ * already performed by phase.cts's `computeDependencyLevels`) before
22
+ * building `PlanHaltNode[]`; this module owns the graph traversal (exactly
23
+ * one pass — Kahn's algorithm, or none at all when the caller already has a
24
+ * valid topological order, see `computeHaltPropagation`'s `precomputedOrder`
25
+ * parameter) plus the two small pure helpers below (`isHaltedStatus`,
26
+ * `buildSummaryFileIndex`) that both callers would otherwise duplicate
27
+ * identically — the exact "two implementations, one drifts" failure mode
28
+ * this fix exists to close. Each caller still owns its own file I/O (the
29
+ * actual `fs.readFileSync` + `extractFrontmatter` calls); only the
30
+ * interpretive logic is centralized here — except the SUMMARY-file
31
+ * read+extract wrapper itself (`isSummaryFileHalted`), which both callers
32
+ * previously duplicated near-identically and which is now centralized here
33
+ * too, for the same reason.
34
+ */
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ const node_fs_1 = __importDefault(require("node:fs"));
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
40
+ const frontmatterMod = require("./frontmatter.cjs");
41
+ const { extractFrontmatter } = frontmatterMod;
42
+ /**
43
+ * The one place "does this SUMMARY status value mean halted" is decided.
44
+ * Case-insensitive, trims whitespace. Both `phase.cts`'s `cmdPhasePlanIndex`
45
+ * and `phase-locator.cts`'s `searchPhaseInDir` call this after reading a
46
+ * completed plan's SUMMARY frontmatter `status` field, so the definition of
47
+ * "halted" cannot drift between the two readers.
48
+ */
49
+ function isHaltedStatus(status) {
50
+ if (typeof status !== 'string')
51
+ return false;
52
+ // #2830 review (defect 2): strip an unquoted trailing YAML comment (a run
53
+ // of whitespace followed by `#` and the rest of the line) before
54
+ // trimming/lowercasing. YAML scalars don't need quoting to carry an inline
55
+ // comment (`status: halted # designed stop`), but `extractFrontmatter`
56
+ // does not strip one — and all four summary templates literally show that
57
+ // spelling as guidance on the value line. Without this, an executor that
58
+ // mimics the template's own presentation would write a halt that silently
59
+ // reads back as not-halted. A `#` with no preceding whitespace is NOT a
60
+ // YAML comment start, so `halted#nospace` intentionally still fails to match.
61
+ const withoutTrailingComment = status.replace(/\s+#.*$/, '');
62
+ return withoutTrailingComment.trim().toLowerCase() === 'halted';
63
+ }
64
+ /**
65
+ * Read a plan's SUMMARY file and report whether it declares `status: halted`
66
+ * (a designed stop, not an ordinary completion). Returns false — never
67
+ * throws — on a missing/unreadable/malformed SUMMARY, so an unreadable file
68
+ * degrades to the pre-#2830 behavior ("has a SUMMARY = complete") rather
69
+ * than breaking either caller.
70
+ *
71
+ * Two callers share this wrapper: `phase.cts`'s `cmdPhasePlanIndex` and
72
+ * `phase-locator.cts`'s `searchPhaseInDir` (the phase-location primitive
73
+ * consumed by ~50 symbols across five command routers). Both previously
74
+ * carried a near-identical local copy (read file -> `extractFrontmatter` ->
75
+ * `isHaltedStatus` -> swallow errors) that this module's own header comment
76
+ * calls out as the exact "two implementations, one drifts" failure mode it
77
+ * exists to prevent — centralizing the read+extract wrapper here, not just
78
+ * the `isHaltedStatus` predicate, closes that gap.
79
+ *
80
+ * Takes a single resolved `summaryPath` (not a `dir` + `filename` pair) —
81
+ * `phase-locator.cts`'s prior local copy took the two parts separately and
82
+ * `path.join`'d them internally; that caller now does the join itself
83
+ * before calling in, so both callers share one signature.
84
+ *
85
+ * @param summaryPath - absolute or relative path to a `*-SUMMARY.md` file.
86
+ */
87
+ function isSummaryFileHalted(summaryPath) {
88
+ try {
89
+ const content = node_fs_1.default.readFileSync(summaryPath, 'utf-8');
90
+ const fm = extractFrontmatter(content, summaryPath);
91
+ return isHaltedStatus(fm['status']);
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * Build a planId -> summary-filename lookup from a phase's summary file
99
+ * list, keyed by both the exact SUMMARY-file-derived id and its canonical
100
+ * form (mirrors the `completedPlanIds` construction each caller already
101
+ * performs for its own SUMMARY-presence check — same `summaryFiles` list,
102
+ * same exact/canonical key pair — so the two can never disagree about which
103
+ * summary file belongs to which plan id). `extractCanonicalPlanId` is
104
+ * supplied by the caller (each module owns its own resolution helper).
105
+ */
106
+ function buildSummaryFileIndex(summaryFiles, extractCanonicalPlanId) {
107
+ const index = new Map();
108
+ for (const s of summaryFiles) {
109
+ const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
110
+ const canonical = extractCanonicalPlanId(s);
111
+ index.set(exact, s);
112
+ if (canonical !== exact)
113
+ index.set(canonical, s);
114
+ }
115
+ return index;
116
+ }
117
+ /**
118
+ * #3345: the one place "does this SUMMARY status value mean blocked" is
119
+ * decided, sibling to `isHaltedStatus` above. A SUMMARY declaring
120
+ * `status: blocked` records a plan that could NOT finish — it is a failure
121
+ * record, not a completion record — so it must not count toward
122
+ * `completed_plans` (scanPhasePlans's summaryCount) nor read as
123
+ * `has_summary: true` in phase-plan-index's `incomplete` construction.
124
+ *
125
+ * `halted` is deliberately NOT matched here: a designed stop still writes a
126
+ * completion record (#2830's model — its dependents get `blocked_by` halt
127
+ * propagation), so a `status: halted` SUMMARY keeps counting as summarized.
128
+ * Case-insensitive, trims whitespace, and strips an unquoted trailing YAML
129
+ * comment exactly like `isHaltedStatus` (same #2830 review defect-2 rule).
130
+ */
131
+ function isBlockedStatus(status) {
132
+ if (typeof status !== 'string')
133
+ return false;
134
+ const withoutTrailingComment = status.replace(/\s+#.*$/, '');
135
+ return withoutTrailingComment.trim().toLowerCase() === 'blocked';
136
+ }
137
+ // #3345: SUMMARY frontmatter sits at byte 0 and closes well before the body,
138
+ // so only a bounded prefix is ever needed to read the `status` marker — the
139
+ // same cap discipline as plan-scan.cts's PLAN_FRONTMATTER_READ_CAP (#2349),
140
+ // kept here because this predicate's primary caller (scanPhasePlans) loops
141
+ // over every phase directory on hot paths (state sync, roadmap progress).
142
+ const SUMMARY_FRONTMATTER_READ_CAP = 64 * 1024;
143
+ /**
144
+ * #3345: read a SUMMARY file's frontmatter `status` (bounded-prefix read) and
145
+ * report whether it declares `status: blocked`. Returns false — never throws —
146
+ * on a missing/unreadable/malformed/non-regular SUMMARY, so an unreadable file
147
+ * degrades to the pre-#3345 filename-existence behaviour rather than breaking
148
+ * either caller. Fail-open by design, mirroring `isPlanSuperseded`'s posture.
149
+ *
150
+ * Callers: plan-scan.cts's `scanPhasePlans` (the count side) and phase.cts's
151
+ * `cmdPhasePlanIndex` (the read side) BOTH filter their summary lists through
152
+ * this one predicate, so the count and the `incomplete` list can never
153
+ * re-diverge on the blocked rule.
154
+ */
155
+ function isSummaryFileBlocked(summaryPath) {
156
+ let content;
157
+ try {
158
+ const st = node_fs_1.default.statSync(summaryPath); // follows symlinks → resolves to the target's real type
159
+ if (!st.isFile())
160
+ return false;
161
+ const length = Math.min(st.size, SUMMARY_FRONTMATTER_READ_CAP);
162
+ if (length === 0)
163
+ return false;
164
+ const fd = node_fs_1.default.openSync(summaryPath, 'r');
165
+ try {
166
+ const buf = Buffer.allocUnsafe(length);
167
+ const bytesRead = node_fs_1.default.readSync(fd, buf, 0, length, 0);
168
+ content = buf.toString('utf8', 0, bytesRead);
169
+ }
170
+ finally {
171
+ node_fs_1.default.closeSync(fd);
172
+ }
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ return isBlockedStatus(extractFrontmatter(content, summaryPath)['status']);
178
+ }
179
+ /**
180
+ * Computes halt-propagation over a plan dependency DAG.
181
+ *
182
+ * `precomputedOrder`: when the caller ALREADY has a valid topological order
183
+ * for these exact node ids (e.g. phase.cts's `cmdPhasePlanIndex`, which runs
184
+ * `computeDependencyLevels`'s Kahn's-algorithm pass for wave assignment
185
+ * before ever calling this function), pass it here and this function skips
186
+ * running Kahn's algorithm a second time — the halt-propagation forward pass
187
+ * below only needs A valid topological order, not to derive one itself.
188
+ * Omit it (as phase-locator.cts's `searchPhaseInDir` does — it has no prior
189
+ * traversal of this DAG) and this function derives the order itself; either
190
+ * way, exactly one Kahn's-algorithm pass runs per caller, never two.
191
+ *
192
+ * Diamond-safe (a plan blocked via two different halted ancestors gets both
193
+ * in its `blockedBy` list, deduplicated) and transitive-safe (a dependent of
194
+ * a dependent of a halted plan is blocked, at any depth).
195
+ */
196
+ function computeHaltPropagation(nodes, precomputedOrder) {
197
+ const byId = new Map(nodes.map((n) => [n.id, n]));
198
+ let order;
199
+ let visited;
200
+ if (precomputedOrder) {
201
+ // Caller already ran Kahn's algorithm over this exact node set (same ids)
202
+ // — trust its order and skip re-deriving one. `visited` mirrors the same
203
+ // "count of nodes reachable in valid topological order" contract a fresh
204
+ // derivation would produce.
205
+ order = precomputedOrder;
206
+ visited = precomputedOrder.length;
207
+ }
208
+ else {
209
+ const inDeg = new Map();
210
+ const adj = new Map(); // dependency id -> dependent ids
211
+ for (const n of nodes) {
212
+ if (!inDeg.has(n.id))
213
+ inDeg.set(n.id, 0);
214
+ if (!adj.has(n.id))
215
+ adj.set(n.id, []);
216
+ for (const depId of n.resolvedDependsOn) {
217
+ if (!byId.has(depId))
218
+ continue; // fail-safe: caller should have filtered these already
219
+ if (!adj.has(depId))
220
+ adj.set(depId, []);
221
+ adj.get(depId).push(n.id);
222
+ inDeg.set(n.id, (inDeg.get(n.id) ?? 0) + 1);
223
+ }
224
+ }
225
+ const queue = [];
226
+ for (const n of nodes) {
227
+ if ((inDeg.get(n.id) ?? 0) === 0)
228
+ queue.push(n.id);
229
+ }
230
+ // Dequeue by head index, not Array.shift() — O(1) amortized, same
231
+ // rationale as computeDependencyLevels (#307).
232
+ let head = 0;
233
+ let v = 0;
234
+ while (head < queue.length) {
235
+ const cur = queue[head++];
236
+ v++;
237
+ for (const dep of adj.get(cur) ?? []) {
238
+ inDeg.set(dep, inDeg.get(dep) - 1);
239
+ if (inDeg.get(dep) === 0)
240
+ queue.push(dep);
241
+ }
242
+ }
243
+ order = queue;
244
+ visited = v;
245
+ }
246
+ // `order` is a valid topological order for every visited node: for edge
247
+ // dep -> dependent, dep appears before dependent. A single forward pass
248
+ // over it — using each node's own already-resolved dependsOn ids —
249
+ // computes blockedBy without any further graph traversal.
250
+ const blockedBy = new Map();
251
+ for (const id of order) {
252
+ const node = byId.get(id);
253
+ if (!node)
254
+ continue;
255
+ const causes = new Set();
256
+ for (const depId of node.resolvedDependsOn) {
257
+ const depNode = byId.get(depId);
258
+ if (!depNode)
259
+ continue;
260
+ if (depNode.halted)
261
+ causes.add(depId);
262
+ const depCauses = blockedBy.get(depId);
263
+ if (depCauses) {
264
+ for (const c of depCauses)
265
+ causes.add(c);
266
+ }
267
+ }
268
+ if (causes.size > 0)
269
+ blockedBy.set(id, Array.from(causes));
270
+ }
271
+ // #2830 review (defect 1): a node involved in a depends_on cycle (or
272
+ // downstream of one) never reaches indegree 0, so it never appears in
273
+ // `order` and the forward pass above never visits it — it would otherwise
274
+ // end up absent from BOTH `blockedBy` and any cycle diagnostic, i.e.
275
+ // reported as ordinary runnable. A node whose position in the topological
276
+ // order is undecidable cannot be shown to be safe to run: silently
277
+ // dropping it is the exact silent-disappearance failure #2830 exists to
278
+ // prevent. Fail closed — give every such non-halted node an explicit,
279
+ // non-empty `blockedBy` entry so no consumer (present or future) can
280
+ // re-admit it as runnable merely by checking "absent from blockedBy".
281
+ // (A node that is itself halted is not "blocked" — it IS the blocker —
282
+ // so it is left out here exactly as the normal forward pass leaves it out.)
283
+ if (order.length < nodes.length) {
284
+ const orderSet = new Set(order);
285
+ for (const n of nodes) {
286
+ if (orderSet.has(n.id) || n.halted)
287
+ continue;
288
+ const causes = Array.from(new Set(n.resolvedDependsOn.filter((depId) => byId.has(depId)))).sort();
289
+ blockedBy.set(n.id, causes.length > 0 ? causes : [n.id]);
290
+ }
291
+ }
292
+ return { order, visited, blockedBy };
293
+ }
294
+ module.exports = {
295
+ computeHaltPropagation,
296
+ isHaltedStatus,
297
+ buildSummaryFileIndex,
298
+ isSummaryFileHalted,
299
+ // #3345: blocked-SUMMARY detection, shared by the count side (scanPhasePlans)
300
+ // and the read side (phase-plan-index) so the two cannot diverge.
301
+ isBlockedStatus,
302
+ isSummaryFileBlocked,
303
+ };
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.AUTHORITY_RUNGS = void 0;
21
21
  exports.getEffectiveAuthority = getEffectiveAuthority;
22
22
  exports.classifyDriftSeverity = classifyDriftSeverity;
23
+ exports.comparePhaseStatus = comparePhaseStatus;
23
24
  /**
24
25
  * Frozen map from authority name to its rung number.
25
26
  *
@@ -115,3 +116,122 @@ function classifyDriftSeverity({ status, authority, }) {
115
116
  return { severity: 'INFO', hardBlock: false };
116
117
  }
117
118
  }
119
+ /**
120
+ * Frozen map from lowercased phase-status text to a shared ordinal rank,
121
+ * covering the union of the STATE.md "Current Position" vocabulary
122
+ * (gsd-core/templates/state.md) and the FULL ROADMAP.md "## Progress" table
123
+ * Status column vocabulary declared by gsd-core/templates/roadmap.md:133 —
124
+ * `Not started | In progress | Complete | Deferred`.
125
+ *
126
+ * The two vocabularies overlap on 'in progress', which is rank 1 in both —
127
+ * no conflict. 'deferred' is rank 0 (no work done) — see comparePhaseStatus's
128
+ * doc comment for how a deferred/non-rank-0 mismatch is classified; it is NOT
129
+ * simply numeric distance from rank 0 like an ordinary lag.
130
+ */
131
+ const PHASE_STATUS_RANKS = Object.freeze({
132
+ // STATE.md "Current Position" vocabulary
133
+ 'ready to plan': 0,
134
+ 'planning': 0,
135
+ 'ready to execute': 1,
136
+ 'in progress': 1,
137
+ 'phase complete': 2,
138
+ // ROADMAP.md "## Progress" table Status column vocabulary
139
+ 'not started': 0,
140
+ 'complete': 2,
141
+ 'deferred': 0,
142
+ });
143
+ /** Rank at which a status asserts work is DONE (terminal, not comparative). */
144
+ const TERMINAL_RANK = 2;
145
+ /**
146
+ * Normalize a raw phase-status string for lookup/comparison: trims
147
+ * surrounding whitespace and lowercases. Returns null for missing/empty
148
+ * values. Single owner of this normalization so `resolvePhaseStatusRank` and
149
+ * the 'deferred' declared-intent check in `comparePhaseStatus` cannot drift
150
+ * apart on what counts as "empty".
151
+ */
152
+ function normalizePhaseStatusText(value) {
153
+ if (value === null || value === undefined)
154
+ return null;
155
+ const normalized = value.trim().toLowerCase();
156
+ return normalized === '' ? null : normalized;
157
+ }
158
+ /**
159
+ * Resolve a raw phase-status string to its shared ordinal rank, or null when
160
+ * the value is missing/empty/unrecognized. Case-insensitive, trims
161
+ * surrounding whitespace.
162
+ *
163
+ * @param value - raw status text from STATE.md or ROADMAP.md
164
+ * @returns the resolved rank, or null if unresolvable
165
+ */
166
+ function resolvePhaseStatusRank(value) {
167
+ const normalized = normalizePhaseStatusText(value);
168
+ if (normalized === null)
169
+ return null;
170
+ if (!Object.prototype.hasOwnProperty.call(PHASE_STATUS_RANKS, normalized))
171
+ return null;
172
+ return PHASE_STATUS_RANKS[normalized];
173
+ }
174
+ /**
175
+ * Compare a phase's status as reported by STATE.md against the same phase's
176
+ * status as reported by ROADMAP.md's "## Progress" table, and classify the
177
+ * result.
178
+ *
179
+ * Unlike classifyDriftSeverity, this never throws for an unrecognized
180
+ * status: the inputs are user document text (prose a human or agent typed
181
+ * into STATE.md/ROADMAP.md), not config, so an unrecognized value is data —
182
+ * surfaced as 'uncheckable' — not a programming error.
183
+ *
184
+ * Rank 2 ('phase complete' / 'Complete') is TERMINAL: it asserts the work is
185
+ * DONE. If exactly one side reports rank 2 and the other does not, that is
186
+ * always 'drifted', regardless of numeric distance — a document claiming
187
+ * "done" while another claims "still going" is a direct contradiction, not
188
+ * mere lag. This is the issue's canonical example: complete in STATE.md but
189
+ * in progress in ROADMAP.md.
190
+ *
191
+ * 'Deferred' is a second declared-intent rank-0 status (gsd-core/templates/
192
+ * roadmap.md:133's full vocabulary: `Not started | In progress | Complete |
193
+ * Deferred`) that is NOT ordinary lag from rank 0: it is an explicit
194
+ * decision to STOP work, not merely "hasn't started yet". If exactly one
195
+ * side declares 'deferred' and the other side's rank is >= 1 (work is
196
+ * reported as in progress or complete), that is always 'drifted' — a phase
197
+ * declared deferred while the other document says work is happening is a
198
+ * direct contradiction, checked here (like the terminal-completeness rule
199
+ * above) BEFORE the numeric distance comparison. 'Deferred' against a
200
+ * rank-0 status on the other side (e.g. 'Not started') stays 'consistent' —
201
+ * both agree no work has happened.
202
+ *
203
+ * Otherwise, ranks are compared numerically: equal → 'consistent';
204
+ * off-by-one → 'lag'; off-by-two-or-more → 'drifted'.
205
+ *
206
+ * @param opts.stateStatus - raw Status value from STATE.md's Current Position
207
+ * @param opts.roadmapStatus - raw Status cell from ROADMAP.md's Progress table
208
+ * @returns { verdict, stateRank, roadmapRank }
209
+ */
210
+ function comparePhaseStatus({ stateStatus, roadmapStatus, }) {
211
+ const stateRank = resolvePhaseStatusRank(stateStatus);
212
+ const roadmapRank = resolvePhaseStatusRank(roadmapStatus);
213
+ if (stateRank === null || roadmapRank === null) {
214
+ return { verdict: 'uncheckable', stateRank, roadmapRank };
215
+ }
216
+ const stateIsTerminal = stateRank === TERMINAL_RANK;
217
+ const roadmapIsTerminal = roadmapRank === TERMINAL_RANK;
218
+ if (stateIsTerminal !== roadmapIsTerminal) {
219
+ return { verdict: 'drifted', stateRank, roadmapRank };
220
+ }
221
+ const stateIsDeferred = normalizePhaseStatusText(stateStatus) === 'deferred';
222
+ const roadmapIsDeferred = normalizePhaseStatusText(roadmapStatus) === 'deferred';
223
+ if (stateIsDeferred !== roadmapIsDeferred) {
224
+ const otherRank = stateIsDeferred ? roadmapRank : stateRank;
225
+ if (otherRank >= 1) {
226
+ return { verdict: 'drifted', stateRank, roadmapRank };
227
+ }
228
+ }
229
+ const diff = Math.abs(stateRank - roadmapRank);
230
+ if (diff === 0) {
231
+ return { verdict: 'consistent', stateRank, roadmapRank };
232
+ }
233
+ if (diff === 1) {
234
+ return { verdict: 'lag', stateRank, roadmapRank };
235
+ }
236
+ return { verdict: 'drifted', stateRank, roadmapRank };
237
+ }
@@ -15,6 +15,12 @@ const { countMatchedSummaries } = coreUtils;
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
16
  const frontmatterMod = require("./frontmatter.cjs");
17
17
  const { extractFrontmatter } = frontmatterMod;
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const planningScopeMod = require("./planning-scope.cjs");
20
+ const { SCOPE } = planningScopeMod;
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
23
+ const { isSummaryFileBlocked } = planDependencyGraphMod;
18
24
  // Excluded derivative files
19
25
  const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
20
26
  const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
@@ -97,6 +103,43 @@ function isRootSummaryFile(fileName) {
97
103
  function isNestedSummaryFile(fileName) {
98
104
  return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
99
105
  }
106
+ /**
107
+ * Strict canonical-naming predicate over a `scanPhasePlans` `planFiles`/
108
+ * `allPlanFiles` ENTRY (root form bare, nested form `plans/`-prefixed, exactly
109
+ * as those arrays store them) — root `<phase>-<NN>-PLAN.md`/bare `PLAN.md`,
110
+ * or nested `plans/PLAN-<NN>....md`/`plans/<x>-PLAN-<NN>....md` — WITHOUT
111
+ * `isRootPlanFile`'s loose `/\.md$/i && /PLAN/i` fallback.
112
+ *
113
+ * The `plans/` prefix check is load-bearing, not cosmetic: `isNestedPlanFile`
114
+ * matches ANY basename containing `-PLAN-<digits>...md` with no anchor
115
+ * requiring an actual `plans/` directory — that shape is exactly the #2893
116
+ * reporter's non-canonical example, `01-PLAN-01-foundation.md`. Applying
117
+ * `isNestedPlanFile` directly to a bare root-level name would therefore
118
+ * misclassify that exact offender as canonical. Only entries scanPhasePlans
119
+ * itself produced with the `plans/` prefix (i.e. read from the real nested
120
+ * subdirectory) are eligible for the nested check.
121
+ *
122
+ * #2893/#3183: `isRootPlanFile`'s loose fallback is deliberately permissive
123
+ * for live-plan COUNTING (a lowercase `plan.md` still counts toward
124
+ * completion — see plan-count-single-owner.test.cjs's pinned case-sensitivity
125
+ * asymmetry). But the #2893 "non-canonical filename" diagnostic (phase.cts's
126
+ * `describeNonCanonicalPlans`, used by find-phase/phase-plan-index/phases
127
+ * list --type plans) exists specifically to CATCH a plan-shaped file that
128
+ * does NOT match the canonical contract and warn instead of silently
129
+ * scheduling it. Feeding that diagnostic (and the `plans`/`files` lists those
130
+ * commands return) the loose `allPlanFiles`/`planFiles` set defeats the
131
+ * diagnostic entirely, since the loose fallback already recognizes the
132
+ * non-canonical file as "matched". This predicate is the STRICT filter those
133
+ * three call sites intersect against so the diagnostic (and what counts as a
134
+ * schedulable plan for those commands specifically) stays canonical-only,
135
+ * while scanPhasePlans's own planCount/summaryCount/completed stay on the
136
+ * loose, permissive rule.
137
+ */
138
+ function isCanonicalPlanFile(fileEntry) {
139
+ if (fileEntry.startsWith('plans/'))
140
+ return isNestedPlanFile(fileEntry.slice('plans/'.length));
141
+ return fileEntry.endsWith('-PLAN.md') || fileEntry === 'PLAN.md';
142
+ }
100
143
  function scanPhasePlans(phaseDir) {
101
144
  let rootFiles;
102
145
  try {
@@ -109,7 +152,9 @@ function scanPhasePlans(phaseDir) {
109
152
  completed: false,
110
153
  hasNestedPlans: false,
111
154
  planFiles: [],
155
+ allPlanFiles: [],
112
156
  summaryFiles: [],
157
+ scope: SCOPE.UNREADABLE,
113
158
  };
114
159
  }
115
160
  const rootPlanFiles = rootFiles.filter(isRootPlanFile);
@@ -117,6 +162,7 @@ function scanPhasePlans(phaseDir) {
117
162
  let nestedPlanFiles = [];
118
163
  let nestedSummaryFiles = [];
119
164
  let hasNestedPlans = false;
165
+ let scope = SCOPE.COMPLETE;
120
166
  const nestedDir = (0, node_path_1.join)(phaseDir, 'plans');
121
167
  if ((0, node_fs_1.existsSync)(nestedDir)) {
122
168
  try {
@@ -125,7 +171,12 @@ function scanPhasePlans(phaseDir) {
125
171
  nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile).map((file) => `plans/${file}`);
126
172
  hasNestedPlans = nestedPlanFiles.length > 0;
127
173
  }
128
- catch { /* ignore unreadable nested layout */ }
174
+ catch {
175
+ // #3183 (ADR-3180 Decision 2): the nested plans/ dir exists but could not
176
+ // be read — this scan cannot see plans it knows are there, so zero is
177
+ // NOT a reliable answer; mark TRUNCATED rather than COMPLETE.
178
+ scope = SCOPE.TRUNCATED;
179
+ }
129
180
  }
130
181
  const allPlanFiles = rootPlanFiles.concat(nestedPlanFiles);
131
182
  // #2349: drop plans explicitly marked `status: superseded` from the plan set
@@ -144,7 +195,18 @@ function scanPhasePlans(phaseDir) {
144
195
  // 30-GAPCLOSURE-SUMMARY.md) must not inflate summary_count or flip a phase to
145
196
  // Complete when plans are still missing summaries. summaryFiles (the array)
146
197
  // still holds every summary on disk for callers that read/list them.
147
- const summaryCount = countMatchedSummaries(planFiles, summaryFiles);
198
+ //
199
+ // #3345: a SUMMARY whose frontmatter declares `status: blocked` is a failure
200
+ // record, not a completion record — it is dropped from the COUNTABLE pairing
201
+ // set before matching. The bounded-prefix status read is the SHARED predicate
202
+ // (plan-dependency-graph.cjs's isSummaryFileBlocked) that phase.cts's read
203
+ // path also filters through, so the count and the `incomplete` list cannot
204
+ // diverge. Fail-open: a SUMMARY with no `status` key, or one that cannot be read,
205
+ // keeps its pre-#3345 filename-existence meaning — untouched projects are
206
+ // byte-for-behaviour identical. `status: halted` stays counted (#2830: a
207
+ // designed stop still writes a completion record).
208
+ const countableSummaryFiles = summaryFiles.filter((f) => !isSummaryFileBlocked((0, node_path_1.join)(phaseDir, f)));
209
+ const summaryCount = countMatchedSummaries(planFiles, countableSummaryFiles);
148
210
  return {
149
211
  planCount,
150
212
  summaryCount,
@@ -155,10 +217,31 @@ function scanPhasePlans(phaseDir) {
155
217
  // (0 >= 0) rather than being pinned below 100% forever, which is the very
156
218
  // failure this fix removes. A genuinely empty phase (no plans authored)
157
219
  // still has allPlanFiles.length 0 and stays not-completed, exactly as before.
220
+ //
221
+ // ADR-3180 §7.4 (issue #3186) — DELIBERATELY NOT routed through
222
+ // `isPhaseComplete` (src/verification.cts). This field answers "are all
223
+ // plans summarized?", NOT "is the phase complete?" — completion
224
+ // additionally requires a passing `*-VERIFICATION.md`, which is the
225
+ // whole point of that owner's unconditional readVerificationStatus call.
226
+ // Folding this field onto `isPhaseComplete` would either over-report
227
+ // completion (a phase whose plans are done but never verified) or drag a
228
+ // verification read into this module, inverting the dependency
229
+ // direction between this Phase-1 owner (plan counting) and the Phase-4
230
+ // owner (completion) — the owner must consume plan counts, never the
231
+ // reverse. Kept as its own, differently-scoped answer per the design's
232
+ // "0.x split" and exempted (function-scoped, not file-scoped) in
233
+ // scripts/lint-completion-predicate-drift.cjs's FUNCTION_SCOPED_EXEMPTIONS.
234
+ // The field name is left unchanged (not renamed to e.g.
235
+ // `summariesMeetPlanCount`) — scanPhasePlans has 11 direct callers, and a
236
+ // rename's blast radius is out of this phase's declared scope; noted
237
+ // here as a deliberate, considered-and-declined option rather than an
238
+ // oversight.
158
239
  completed: allPlanFiles.length > 0 && summaryCount >= planCount,
159
240
  hasNestedPlans,
160
241
  planFiles,
242
+ allPlanFiles,
161
243
  summaryFiles,
244
+ scope,
162
245
  };
163
246
  }
164
247
  module.exports = Object.assign(scanPhasePlans, {
@@ -167,4 +250,5 @@ module.exports = Object.assign(scanPhasePlans, {
167
250
  isNestedPlanFile,
168
251
  isRootSummaryFile,
169
252
  isNestedSummaryFile,
253
+ isCanonicalPlanFile,
170
254
  });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Planning Scope — shared result discriminator for live-plan scanning.
4
+ *
5
+ * ADR-3180 Decision 2 (docs/adr/3180-planning-semantic-model-single-owner.md):
6
+ * `scanPhasePlans` is the single owner of live-plan counting, and every
7
+ * consumer of that count needs to distinguish a REAL answer from a
8
+ * NON-answer. `SCOPE.COMPLETE` with zero items is a real answer — a phase
9
+ * genuinely has no plans yet. `SCOPE.TRUNCATED`, `SCOPE.UNSCOPED`, and
10
+ * `SCOPE.UNREADABLE` with zero items are NOT — they mean the scan could not
11
+ * see (part of) the phase directory, so a caller must not treat that zero as
12
+ * "this phase has no plans."
13
+ *
14
+ * This is a frozen enum, not a message string: CONTRIBUTING.md bans raw-text
15
+ * matching on outputs and requires a typed IR, so callers branch on the
16
+ * `SCOPE` value rather than pattern-matching prose.
17
+ *
18
+ * PROVISIONAL: this contract is pending this phase's validation and may be
19
+ * revised before the epic (#3180) ships.
20
+ *
21
+ * Dependencies: none — this is a leaf module (mirrors src/phase-id.cts). It
22
+ * imports nothing, so any consumer can depend on it without risking a cycle.
23
+ */
24
+ const SCOPE = Object.freeze({
25
+ COMPLETE: 'complete',
26
+ TRUNCATED: 'truncated',
27
+ UNSCOPED: 'unscoped',
28
+ UNREADABLE: 'unreadable',
29
+ });
30
+ const planningScope = { SCOPE };
31
+ module.exports = planningScope;