@opengsd/gsd-core 1.10.0 → 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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -16,30 +16,23 @@ exports.pickRollupWinners = pickRollupWinners;
16
16
  exports.isCompletedInventory = isCompletedInventory;
17
17
  exports.buildWorkstreamInventory = buildWorkstreamInventory;
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
+ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
19
20
  // Internal helpers
20
21
  function toPosixPath(p) {
21
22
  return p.split('\\').join('/');
22
23
  }
23
- /**
24
- * #2562: verification verdicts that DISQUALIFY a phase from `complete`, even
25
- * when its SUMMARY count meets its PLAN count. Deliberately scoped to the two
26
- * EXPLICIT failing verdicts the verifier emits — `missing`/`unknown` (verifier
27
- * off / not yet run) and `stale` (mtime-derived, #2348) are intentionally left
28
- * untouched so verifier-disabled projects do not regress to never-complete.
29
- *
30
- * #2645: `'unrecorded'` is NOT a verdict the verifier itself ever emits — it
31
- * is an internal sentinel `workstream-inventory.cts`'s verification-deletion
32
- * ledger substitutes for `'missing'` once that workstream has ADOPTED the
33
- * ledger (a `.verification-ledger.json` file exists for it) but has no
34
- * remembered entry for this specific phase. Pre-adoption (no ledger file at
35
- * all) still resolves to plain `'missing'`, which stays OUTSIDE this set —
36
- * that is what keeps a project untouched by this fix until it actually uses
37
- * the verifier at least once. Post-adoption, an unrecorded phase fails
38
- * CLOSED (counted here) rather than open, so a corrupt or evidence-absent
39
- * ledger entry can no longer be read as "safe to complete" the way a bare
40
- * `'missing'` sentinel is.
41
- */
42
- const FAILING_VERIFICATION_STATUSES = new Set(['gaps_found', 'human_needed', 'unrecorded']);
24
+ // #2562/#2645's FAILING_VERIFICATION_STATUSES set (the verdicts that used to
25
+ // disqualify a phase from `complete` when combined with a local
26
+ // summary-count-meets-plan-count check) was removed by ADR-3180 §7.4
27
+ // (#3186): `complete` is now the single canonical owner's verdict
28
+ // (`PhaseFilesCount.complete`, computed via `isPhaseComplete` by the
29
+ // I/O-capable caller — see the loop below), which already requires
30
+ // `verification.status === 'passed'` unconditionally. Disk-strict (#2957)
31
+ // deliberately DROPS the prior "verifier-disabled projects fall back to
32
+ // summaries-met" tolerance that set existed to preserve — a phase with no
33
+ // `*-VERIFICATION.md` (`missing`) is no longer treated as complete just
34
+ // because its summaries meet its plan count. Disclosed in this phase's
35
+ // changeset.
43
36
  /**
44
37
  * #2562 / Bug #2445 / #2645 review: pick ONE winning item per key from a
45
38
  * PRE-SORTED list — newest `mtimeMs` wins; on an exact tie the incumbent
@@ -127,11 +120,20 @@ function buildWorkstreamInventory(inputs) {
127
120
  const counts = countsMap.get(dir);
128
121
  const planCount = counts?.planCount ?? 0;
129
122
  const summaryCount = counts?.summaryCount ?? 0;
130
- // #2562: SUMMARY≥PLAN parity is necessary but not sufficient — a phase whose
131
- // verification verdict is an explicit failing one is still in progress.
132
- const verificationStatus = counts?.verificationStatus ?? 'missing';
133
- const summariesMeetPlans = summaryCount >= planCount && planCount > 0;
134
- const status = summariesMeetPlans && !FAILING_VERIFICATION_STATUSES.has(verificationStatus)
123
+ // ADR-3180 §7.4 (issue #3186): routed through the single canonical owner
124
+ // (`isPhaseComplete`, src/verification.cts) — via `PhaseFilesCount.complete`,
125
+ // which the I/O-capable CALLER computes (this module is a PURE, I/O-free
126
+ // projection — see the module header: "No I/O. No async." — and cannot
127
+ // call the owner itself). The prior local derivation
128
+ // (`summaryCount >= planCount && planCount > 0` combined with a
129
+ // caller-supplied verification status) was this module's OWN completion
130
+ // verdict computed from raw counts — the exact "post-process a canonical
131
+ // result locally" bypass §7.4 rules out, and it reproduced the disk-strict
132
+ // headline case (#3168): a zero-plan phase with a passing verification
133
+ // read `pending` instead of `complete`. `complete` defaults to `false`
134
+ // when absent so a caller that has not been updated to pass it never
135
+ // silently reads as complete.
136
+ const status = (counts?.complete ?? false)
135
137
  ? 'complete'
136
138
  : planCount > 0
137
139
  ? 'in_progress'
@@ -243,12 +245,31 @@ function buildWorkstreamInventory(inputs) {
243
245
  roadmap_phase_count: effectivePhaseCount,
244
246
  total_plans: totalPlans,
245
247
  completed_plans: completedPlans,
246
- // The `Math.min` cap is unreachable under milestone scoping (the invariant
247
- // above throws first) and survives only for the legacy unscoped path, where
248
- // the denominator is a roadmap heading count that a caller cannot guarantee
249
- // bounds the numerator.
250
- progress_percent: effectivePhaseCount > 0
251
- ? Math.min(100, Math.round((completedPhases / effectivePhaseCount) * 100))
252
- : 0,
248
+ // `clampPercent`'s 100 ceiling is unreachable under milestone scoping (the
249
+ // invariant above throws first) and matters only for the legacy unscoped
250
+ // path, where the denominator is a roadmap heading count that a caller
251
+ // cannot guarantee bounds the numerator.
252
+ //
253
+ // #3217 (ADR-3180 §7.6 rule 4) — WRITTEN REASON this site is NOT migrated
254
+ // onto the `SCOPE` enum this phase: `buildWorkstreamInventory` is a pure
255
+ // projection (no I/O — see the module header) fed `BuildWorkstreamInventoryInputs`
256
+ // by `workstream-inventory.cts`. Its own `milestoneScoped` is a pre-ADR-3180
257
+ // bespoke boolean, not a `SCOPE` value, and its caller does not currently
258
+ // thread a real `listMilestonePhaseDirs` scope into these inputs. Doing
259
+ // this honestly requires ONE of: (a) widening `BuildWorkstreamInventoryInputs`
260
+ // with a `Scope` field and `WorkstreamInventory.progress_percent`'s type
261
+ // from `number` to `number | null` — the exact "re-architecting
262
+ // StateProjection/WorkstreamInventory return types" the design phase
263
+ // (`.gsd/phase/refactor-3217-completion-ratio-scoping/40-design.md`,
264
+ // "Known limits") states is OUT of this phase's scope; or (b) silently
265
+ // reusing `milestoneScoped` as a `Scope` stand-in, which would be exactly
266
+ // the kind of proxy-for-a-data-flow-property this same phase's guard
267
+ // section explicitly rejects (a `boolean` cannot distinguish TRUNCATED
268
+ // from UNSCOPED from UNREADABLE, so a caller could not tell which
269
+ // non-answer it got). Left un-migrated rather than done dishonestly;
270
+ // `workstream inventory`'s `progress_percent` can still render a number
271
+ // derived from an under-scoped set (A8 in the phase's test matrix is
272
+ // NOT covered here for that reason — see this phase's PR description).
273
+ progress_percent: (0, phase_lifecycle_cjs_1.clampPercent)(completedPhases, effectivePhaseCount),
253
274
  };
254
275
  }
@@ -27,10 +27,13 @@ const planScan = require("./plan-scan.cjs");
27
27
  const planningWorkspace = require("./planning-workspace.cjs");
28
28
  const { planningPaths, planningRoot, getActiveWorkstream } = planningWorkspace;
29
29
  const state_document_cjs_1 = require("./state-document.cjs");
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
31
+ const frontmatterMod = require("./frontmatter.cjs");
32
+ const { extractFrontmatter, stripFrontmatter } = frontmatterMod;
30
33
  const markdown_table_cjs_1 = require("./markdown-table.cjs");
31
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
32
35
  const verificationMod = require("./verification.cjs");
33
- const { readVerificationStatus } = verificationMod;
36
+ const { isPhaseComplete } = verificationMod;
34
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
35
38
  const phaseIdMod = require("./phase-id.cjs");
36
39
  const { phaseKeyFromDir, phaseKeyFromProse, parentPhaseKey } = phaseIdMod;
@@ -42,11 +45,28 @@ const workstream_inventory_builder_cjs_1 = require("./workstream-inventory-build
42
45
  function workstreamsRoot(cwd) {
43
46
  return node_path_1.default.join(planningRoot(cwd), 'workstreams');
44
47
  }
45
- function countRoadmapPhases(roadmapPath, fallbackCount) {
48
+ /**
49
+ * #3185 (ADR-3180 Decision 1): count the phases the CURRENT milestone
50
+ * declares, not every `Phase` heading in the file.
51
+ *
52
+ * This previously matched `^#{2,4}\s+Phase\s+…` across the whole ROADMAP with
53
+ * no milestone window and no sentinel filter, so it counted 999.* backlog and
54
+ * Phase 0 headings and spanned every milestone the document had ever had.
55
+ * `getMilestonePhaseFilter` already computes exactly this number for the
56
+ * scoped window (`phaseCount`, sentinel-filtered), and `inspectWorkstream` in
57
+ * this same file already passes a resolved `currentVersion` to it — this
58
+ * function was the sibling copy that never got the fix.
59
+ */
60
+ function countRoadmapPhases(roadmapPath, fallbackCount, cwd, ws, versionOverride) {
46
61
  try {
47
- const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
48
- const matches = roadmapContent.match(/^#{2,4}\s+Phase\s+[\w][\w.-]*/gm);
49
- return matches ? matches.length : fallbackCount;
62
+ if (!node_fs_1.default.existsSync(roadmapPath))
63
+ return fallbackCount;
64
+ if (!cwd)
65
+ return fallbackCount;
66
+ const filter = getMilestonePhaseFilter(cwd, versionOverride ?? null, null, ws ?? null);
67
+ // A pass-all degrade (phaseCount 0) means the window declared no phases —
68
+ // fall back rather than reporting a confident zero.
69
+ return filter.phaseCount > 0 ? filter.phaseCount : fallbackCount;
50
70
  }
51
71
  catch {
52
72
  return fallbackCount;
@@ -312,13 +332,30 @@ function writeVerificationLedger(wsDir, ledger) {
312
332
  function readStateProjection(statePath) {
313
333
  try {
314
334
  const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
335
+ // #3187: route Status/Current Phase/Last Activity through the single
336
+ // #1760 fallback-chain owner (state-document.cjs's stateFieldValue)
337
+ // instead of a frontmatter-blind stateExtractField(stateContent, …) call,
338
+ // mirroring cmdStateValidate/cmdStateSnapshot — a STATE.md whose fields
339
+ // live only in frontmatter is no longer projected as absent here.
340
+ const fm = extractFrontmatter(stateContent, statePath);
341
+ const body = stripFrontmatter(stateContent);
315
342
  return {
316
- status: (0, state_document_cjs_1.stateExtractField)(stateContent, 'Status') || 'unknown',
317
- current_phase: (0, state_document_cjs_1.stateExtractField)(stateContent, 'Current Phase'),
318
- last_activity: (0, state_document_cjs_1.stateExtractField)(stateContent, 'Last Activity'),
343
+ status: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'status', 'Status').value || 'unknown',
344
+ current_phase: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'current_phase', 'Current Phase').value,
345
+ last_activity: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'last_activity', 'Last Activity').value,
319
346
  };
320
347
  }
321
348
  catch {
349
+ // Read/parse failure (missing file, permission fault, etc.) degrades to
350
+ // an all-unknown projection — unchanged. Not widened to also carry a
351
+ // `scope` here: doing so would ripple `StateProjection`
352
+ // (workstream-inventory-builder.cjs) and every consumer of this
353
+ // read-only rollup — the design doc's blast-radius table rates
354
+ // `readStateProjection` "low"/Tier-2, and this call site's migration is
355
+ // scoped to routing the fallback chain, not to widening the return type.
356
+ // The existing all-unknown degrade already distinguishes "could not
357
+ // read" from any real field value; only its scope-vs-absence *reason*
358
+ // stays uncaptured, same as before this change.
322
359
  return {
323
360
  status: 'unknown',
324
361
  current_phase: null,
@@ -483,7 +520,16 @@ function inspectWorkstream(cwd, name, options = {}) {
483
520
  const rawPhaseEntries = [...phaseDirNames].sort().map(dir => {
484
521
  const phaseDir = node_path_1.default.join(p.phases, dir);
485
522
  const counts = countPhaseFiles(phaseDir);
486
- const verificationResult = readVerificationStatus(phaseDir);
523
+ // ADR-3180 §7.4 (#3186): routed through the single canonical owner
524
+ // (`isPhaseComplete`, src/verification.cts) instead of calling
525
+ // `readVerificationStatus` directly and re-deriving "is this phase
526
+ // complete" locally from its `.status`. `completionResult.value.complete`
527
+ // is threaded down to the builder below (as `PhaseFilesCount.complete`)
528
+ // so `buildWorkstreamInventory` — a pure, I/O-free projection that
529
+ // cannot call the owner itself — consumes the owner's verdict rather
530
+ // than re-deriving a second one from summary/plan counts.
531
+ const completionResult = isPhaseComplete(phaseDir);
532
+ const verificationResult = completionResult.value.verification;
487
533
  // #3057 B3: routing is UNCHANGED — `liveVerificationStatus` below is still
488
534
  // `.status`, exactly as before, so the ledger/rollup logic that consumes
489
535
  // it is unaffected. This only makes an indeterminate staleness check
@@ -502,6 +548,12 @@ function inspectWorkstream(cwd, name, options = {}) {
502
548
  summaryCount: counts.summaryCount,
503
549
  inMilestone: isDirInCurrentMilestone(dir),
504
550
  liveVerificationStatus: verificationResult.status,
551
+ // ADR-3180 §7.4 (#3186): the owner's verdict, read live off disk — never
552
+ // ledger-adjusted (see the phaseFilesCounts map below; the ledger only
553
+ // ever substitutes a 'missing' status with a remembered one, and under
554
+ // disk-strict neither 'missing' nor 'unrecorded' is ever complete, so
555
+ // there is nothing for the ledger to override here).
556
+ complete: completionResult.value.complete,
505
557
  };
506
558
  });
507
559
  // #2645: only the directory Bug #2445's de-dup rollup would actually pick
@@ -573,6 +625,7 @@ function inspectWorkstream(cwd, name, options = {}) {
573
625
  summaryCount: entry.summaryCount,
574
626
  inMilestone: entry.inMilestone,
575
627
  verificationStatus,
628
+ complete: entry.complete,
576
629
  };
577
630
  });
578
631
  // The denominator is the union of what the roadmap DECLARES for the current
@@ -592,7 +645,7 @@ function inspectWorkstream(cwd, name, options = {}) {
592
645
  // declares in its Progress table but never scaffolded — the heading-only
593
646
  // count drops them, even when other headings exist. Union the declared rows
594
647
  // with the phase directories so neither source can silently shrink it.
595
- let fallbackPhaseCount = countRoadmapPhases(p.roadmap, phaseDirNames.length);
648
+ let fallbackPhaseCount = countRoadmapPhases(p.roadmap, phaseDirNames.length, cwd, name, currentVersion);
596
649
  if (!scoped && progressRows.length > 0) {
597
650
  const union = new Set(progressRows.map(row => row.key));
598
651
  for (const entry of phaseFilesCounts)
@@ -134,8 +134,8 @@ function cmdWorkstreamCreate(cwd, name, options, raw) {
134
134
  }
135
135
  else {
136
136
  try {
137
- const milestone = getMilestoneInfo(cwd);
138
- existingWsName = generateSlugInternal(milestone.name) || 'default';
137
+ const milestone = getMilestoneInfo(cwd).value;
138
+ existingWsName = generateSlugInternal(milestone?.name ?? null) || 'default';
139
139
  }
140
140
  catch {
141
141
  existingWsName = 'default';
@@ -366,13 +366,24 @@ function normalizeCleanupManifestEntry(entry) {
366
366
  return null;
367
367
  const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
368
368
  const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
369
- return {
369
+ // #2596: liberal in what we accept — non-array, or non-string / empty
370
+ // elements, are dropped rather than coerced. An EMPTY result omits the field
371
+ // entirely, so "declared nothing" is indistinguishable from "not recorded":
372
+ // that ambiguity is already resolved as *unknown* by the per-plan submodule
373
+ // gate's own `[ -z "$PLAN_FILES" ]` rule, and inventing a second rule here
374
+ // would make an unrecorded plan look 100% out of scope.
375
+ const filesModified = (Array.isArray(e.files_modified) ? e.files_modified : [])
376
+ .filter((f) => typeof f === 'string' && f.trim().length > 0);
377
+ const normalized = {
370
378
  agent_id: typeof e.agent_id === 'string' ? e.agent_id : null,
371
379
  worktree_path: worktreePath,
372
380
  branch,
373
381
  expected_base: expectedBase,
374
382
  allowed_bases: allowedBases,
375
383
  };
384
+ if (filesModified.length > 0)
385
+ normalized.files_modified = filesModified;
386
+ return normalized;
376
387
  }
377
388
  function normalizeCleanupManifest(manifest) {
378
389
  let parsed = manifest;
@@ -461,6 +472,39 @@ function repoRootStillMidMerge(execGit, repoRoot) {
461
472
  return false; // ref not found — repoRoot is not mid-merge
462
473
  return true; // any other exit code (e.g. a fatal git error) — fail closed
463
474
  }
475
+ // #2596: the single definition of "this file is an executor-written SUMMARY
476
+ // artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
477
+ // rescue) and the scope advisory below (which must never flag them) — a plan's
478
+ // declared `files_modified` never lists a SUMMARY, because the executor writes
479
+ // it by orchestration contract, so a second copy of this rule would make the
480
+ // advisory fire on essentially every wave.
481
+ const SUMMARY_ARTIFACT_DIR = '.planning';
482
+ const SUMMARY_ARTIFACT_SUFFIX = 'SUMMARY.md';
483
+ /**
484
+ * Normalize one path for scope comparison. Applied to BOTH sides so a declared
485
+ * path and a git-reported path meet in the same shape: backslashes become
486
+ * slashes unconditionally (a backslash path is not a Windows-only input),
487
+ * a leading `./` and any trailing `/` are stripped. This is the single
488
+ * normalizer shared by the SUMMARY-artifact predicate and the scope advisory,
489
+ * so the two can never disagree about what `./a\b/` means.
490
+ */
491
+ function normalizeScopePath(raw) {
492
+ return String(raw || '')
493
+ .replace(/\\/g, '/')
494
+ .trim()
495
+ .replace(/^\.\//, '')
496
+ .replace(/\/+$/, '');
497
+ }
498
+ /**
499
+ * True when a worktree-relative path is a SUMMARY artifact. Input may use
500
+ * either separator; normalization to POSIX is unconditional (backslash paths
501
+ * reach Linux too).
502
+ */
503
+ function isSummaryArtifactRelPath(relPath) {
504
+ const normalized = normalizeScopePath(relPath);
505
+ return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`)
506
+ && normalized.endsWith(SUMMARY_ARTIFACT_SUFFIX);
507
+ }
464
508
  /**
465
509
  * Walk <worktreePath>/.planning/ recursively and collect absolute paths of
466
510
  * all files whose names match *SUMMARY.md. Returns [] when the directory
@@ -470,7 +514,7 @@ function repoRootStillMidMerge(execGit, repoRoot) {
470
514
  * find "$WT/.planning" -name "*SUMMARY.md"
471
515
  */
472
516
  function defaultFindSummaryFiles(worktreePath) {
473
- const planningDir = node_path_1.default.join(worktreePath, '.planning');
517
+ const planningDir = node_path_1.default.join(worktreePath, SUMMARY_ARTIFACT_DIR);
474
518
  const results = [];
475
519
  function walk(dir) {
476
520
  let entries;
@@ -485,7 +529,7 @@ function defaultFindSummaryFiles(worktreePath) {
485
529
  if (entry.isDirectory()) {
486
530
  walk(full);
487
531
  }
488
- else if (entry.isFile() && entry.name.endsWith('SUMMARY.md')) {
532
+ else if (entry.isFile() && entry.name.endsWith(SUMMARY_ARTIFACT_SUFFIX)) {
489
533
  results.push(full);
490
534
  }
491
535
  }
@@ -588,6 +632,77 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
588
632
  }
589
633
  return { rescuedRelPaths, failures };
590
634
  }
635
+ /**
636
+ * #2596: advisory codes emitted by the wave-cleanup gauntlet. Frozen and
637
+ * exported so tests assert on a code rather than on rendered prose (this repo
638
+ * forbids raw-text matching on test output).
639
+ */
640
+ const WAVE_CLEANUP_WARNING = Object.freeze({
641
+ /** A committed path fell outside the plan's declared `files_modified`. */
642
+ SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
643
+ /** The scope diff could not be computed, so conformance is unknown. */
644
+ SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
645
+ });
646
+ /**
647
+ * The literal directory prefix a declared path covers, or `null` when the
648
+ * pattern begins with a glob metacharacter and therefore has no usable prefix.
649
+ *
650
+ * Deliberately literal-prefix only — NOT a glob engine. A hand-rolled
651
+ * `**`/`*`/`?` matcher inside a worktree-lifecycle module is an informal,
652
+ * undocumented pattern language living where no language belongs, and this
653
+ * repo forbids external deps in core. The submodule-intersection gate
654
+ * (`workflows/execute-phase/steps/per-plan-worktree-gate.md`) already ships
655
+ * exactly this glob-prefix rule; reusing it beats inventing a second one.
656
+ *
657
+ * `null` (no literal prefix, e.g. `*.md`) means "matches everything": for an
658
+ * ADVISORY, a false alarm costs more than a miss, so the ambiguous case
659
+ * suppresses rather than shouts.
660
+ */
661
+ function declaredScopePrefix(declared) {
662
+ const globAt = declared.search(/[*?[]/);
663
+ if (globAt < 0)
664
+ return declared;
665
+ const literal = declared.slice(0, globAt).replace(/\/+$/, '');
666
+ return literal.length > 0 ? literal : null;
667
+ }
668
+ /**
669
+ * #2596: compare a branch's actual committed paths against the plan's declared
670
+ * scope. Pure — no git, no IO. Returns one warning per out-of-scope path, in
671
+ * the order the paths were given. Returns [] when nothing usable was declared:
672
+ * absence of data is not evidence of over-reach.
673
+ */
674
+ function planWaveScopeConformance(changedPaths, declaredFiles, branch) {
675
+ if (!Array.isArray(declaredFiles))
676
+ return [];
677
+ const prefixes = [];
678
+ for (const declared of declaredFiles) {
679
+ if (typeof declared !== 'string')
680
+ continue;
681
+ const normalized = normalizeScopePath(declared);
682
+ if (!normalized)
683
+ continue;
684
+ prefixes.push(declaredScopePrefix(normalized));
685
+ }
686
+ if (prefixes.length === 0)
687
+ return [];
688
+ const warnings = [];
689
+ const seen = new Set();
690
+ for (const raw of Array.isArray(changedPaths) ? changedPaths : []) {
691
+ if (typeof raw !== 'string')
692
+ continue;
693
+ const changed = normalizeScopePath(raw);
694
+ if (!changed || seen.has(changed))
695
+ continue;
696
+ seen.add(changed);
697
+ if (isSummaryArtifactRelPath(changed))
698
+ continue;
699
+ const covered = prefixes.some((prefix) => (prefix === null || changed === prefix || changed.startsWith(`${prefix}/`)));
700
+ if (covered)
701
+ continue;
702
+ warnings.push({ code: WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED, branch, path: changed });
703
+ }
704
+ return warnings;
705
+ }
591
706
  function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
592
707
  const execGit = deps.execGit || execGitDefault;
593
708
  const entries = Array.isArray(plan?.entries) ? plan.entries : [];
@@ -598,10 +713,12 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
598
713
  reason: plan ? (plan.reason || 'missing_entries') : 'missing_plan',
599
714
  entries: [],
600
715
  pending: entries,
716
+ warnings: [],
601
717
  };
602
718
  }
603
719
  const results = [];
604
720
  const pending = [];
721
+ const allWarnings = [];
605
722
  let ok = true;
606
723
  // #2852: every per-entry failure site marks the SAME shape — status='blocked',
607
724
  // a reason code, the captured stderr, push to results, flip the overall `ok`
@@ -622,6 +739,7 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
622
739
  status: 'pending',
623
740
  reason: null,
624
741
  stderr: '',
742
+ warnings: [],
625
743
  };
626
744
  const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
627
745
  if (!gitResultOk(branchCheck) || branchCheck.stdout.trim() !== entry.branch) {
@@ -652,6 +770,25 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
652
770
  blockEntry(result, 'branch_contains_deletions', deletions.stdout);
653
771
  continue; // #2852: isolate
654
772
  }
773
+ // #2596: advisory scope conformance — does the branch's ACTUAL committed
774
+ // diff stay inside the scope the plan declared? Gated on a declared scope
775
+ // being present: with nothing declared there is nothing to compare, so no
776
+ // git subprocess is spent at all (and every pre-#2596 fixture, none of
777
+ // which declares one, issues exactly the git calls it always did).
778
+ //
779
+ // ADVISORY ONLY. Unlike the deletions check above, a finding here does NOT
780
+ // call blockEntry and does NOT touch `ok` — the merge proceeds. Promotion
781
+ // to a hard gate is a separate, disclosed change.
782
+ if (Array.isArray(entry.files_modified) && entry.files_modified.length > 0) {
783
+ const scopeDiff = execGit(['diff', '--name-only', `HEAD...${entry.branch}`], { cwd: plan.repoRoot });
784
+ const scopeWarnings = !gitResultOk(scopeDiff)
785
+ // A broken advisory must never become a gate: record that conformance
786
+ // is unknown rather than blocking (or, worse, silently passing).
787
+ ? [{ code: WAVE_CLEANUP_WARNING.SCOPE_CHECK_UNAVAILABLE, branch: entry.branch, path: null }]
788
+ : planWaveScopeConformance((scopeDiff.stdout || '').split('\n'), entry.files_modified, entry.branch);
789
+ result.warnings.push(...scopeWarnings);
790
+ allWarnings.push(...scopeWarnings);
791
+ }
655
792
  // Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
656
793
  // The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
657
794
  // orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
@@ -737,6 +874,7 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
737
874
  reason: ok ? 'ok' : 'cleanup_blocked',
738
875
  entries: results,
739
876
  pending,
877
+ warnings: allWarnings,
740
878
  };
741
879
  }
742
880
  function cmdWorktreeCleanupWave(cwd, args = []) {
@@ -777,6 +915,15 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
777
915
  process.exitCode = 1;
778
916
  }
779
917
  }
918
+ /**
919
+ * #2596: split a `--files` value into declared paths. Whitespace-separated,
920
+ * matching the `PLAN_FILES` shape the per-plan worktree gate already builds
921
+ * with `jq -r '.files_modified // [] | join(" ")'`. Values are DATA compared
922
+ * against a diff — never opened, never passed to a shell.
923
+ */
924
+ function parseDeclaredScopeFlag(raw) {
925
+ return String(raw || '').split(/\s+/).filter((token) => token.length > 0);
926
+ }
780
927
  /**
781
928
  * Pure planner for the per-agent wave-manifest append.
782
929
  *
@@ -793,8 +940,9 @@ function cmdWorktreeCleanupWave(cwd, args = []) {
793
940
  * rejected loudly — the reader dedups on that key, so a re-record would be
794
941
  * silently dropped (the failure mode this verb exists to eliminate). The
795
942
  * on-disk shape stays the existing 4-field entry (`agent_id`, `worktree_path`,
796
- * `branch`, `expected_base`) — no schema change; the reader re-derives
797
- * `allowed_bases`.
943
+ * `branch`, `expected_base`) unless `--files` declares a scope, in which case
944
+ * an optional `files_modified` is appended (#2596); the reader still
945
+ * re-derives `allowed_bases`.
798
946
  */
799
947
  function planWorktreeRecordAgent(manifestRaw, fields) {
800
948
  // 1. Write-strict required-field check (loud, with which flag is missing).
@@ -824,11 +972,13 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
824
972
  }
825
973
  // 2. Shared validation: run the candidate through the reader's normalizer.
826
974
  // If it returns null the reader would drop this entry on read — reject now.
975
+ const declaredScope = parseDeclaredScopeFlag(fields.files);
827
976
  const candidate = {
828
977
  agent_id: agentId,
829
978
  worktree_path: worktreePath,
830
979
  branch,
831
980
  expected_base: base,
981
+ ...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
832
982
  };
833
983
  const entry = normalizeCleanupManifestEntry(candidate);
834
984
  if (!entry) {
@@ -917,6 +1067,11 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
917
1067
  branch: entry.branch,
918
1068
  expected_base: entry.expected_base,
919
1069
  };
1070
+ // #2596: only written when a scope was actually declared — conservative in
1071
+ // what we send, so a blank --files leaves the 4-field shape untouched.
1072
+ if (entry.files_modified && entry.files_modified.length > 0) {
1073
+ recorded.files_modified = entry.files_modified;
1074
+ }
920
1075
  worktrees.push(recorded);
921
1076
  return {
922
1077
  ok: true,
@@ -928,7 +1083,7 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
928
1083
  /**
929
1084
  * CLI command: append a validated per-agent entry to a wave cleanup manifest.
930
1085
  *
931
- * Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
1086
+ * Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]
932
1087
  *
933
1088
  * Fails loudly (non-zero exit + recovery hint on stderr) when a field is
934
1089
  * missing/garbled or the manifest is absent/malformed, rather than appending an
@@ -943,7 +1098,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
943
1098
  const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
944
1099
  const manifestPath = flag('--manifest');
945
1100
  if (!manifestPath) {
946
- writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>\n');
1101
+ writeErr('Usage: worktree record-agent --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--files "<space-separated paths>"]\n');
947
1102
  process.exitCode = 2;
948
1103
  return { ok: false, reason: 'usage', entry: null };
949
1104
  }
@@ -965,6 +1120,7 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
965
1120
  worktreePath: flag('--path'),
966
1121
  branch: flag('--branch'),
967
1122
  base: flag('--base'),
1123
+ files: flag('--files'),
968
1124
  });
969
1125
  if (!plan.ok || plan.manifest === null) {
970
1126
  writeErr(`[gsd] worktree.record-agent: ${plan.reason} — ${plan.hint || ''}\n`);
@@ -1008,11 +1164,13 @@ function planWorktreeCreate(fields) {
1008
1164
  entry: null,
1009
1165
  };
1010
1166
  }
1167
+ const declaredScope = parseDeclaredScopeFlag(fields.files);
1011
1168
  const candidate = {
1012
1169
  agent_id: agentId,
1013
1170
  worktree_path: worktreePath,
1014
1171
  branch,
1015
1172
  expected_base: base,
1173
+ ...(declaredScope.length > 0 ? { files_modified: declaredScope } : {}),
1016
1174
  };
1017
1175
  const entry = normalizeCleanupManifestEntry(candidate);
1018
1176
  if (!entry) {
@@ -1140,7 +1298,7 @@ function executeWorktreeCreatePlan(plan, repoRoot, deps = {}) {
1140
1298
  * validated manifest entry so the worktree is immediately manageable by
1141
1299
  * `worktree cleanup-wave` / `worktree reap-orphans`.
1142
1300
  *
1143
- * Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir>
1301
+ * Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]
1144
1302
  *
1145
1303
  * #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
1146
1304
  * plan step runs BEFORE the git side effect (step 5). The ONLY manifest
@@ -1160,7 +1318,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
1160
1318
  const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
1161
1319
  const manifestPath = flag('--manifest');
1162
1320
  if (!manifestPath) {
1163
- writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir>\n');
1321
+ writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> --root <dir> [--files "<space-separated paths>"]\n');
1164
1322
  process.exitCode = 2;
1165
1323
  return { ok: false, reason: 'usage' };
1166
1324
  }
@@ -1225,6 +1383,7 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
1225
1383
  worktreePath: flag('--path'),
1226
1384
  branch: flag('--branch'),
1227
1385
  base: flag('--base'),
1386
+ files: flag('--files'),
1228
1387
  });
1229
1388
  if (!plan.ok || !plan.entry) {
1230
1389
  writeErr(`[gsd] worktree.create: ${plan.reason} — ${plan.hint || ''}\n`);
@@ -1291,6 +1450,11 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
1291
1450
  branch: plan.entry.branch,
1292
1451
  expected_base: plan.entry.expected_base,
1293
1452
  };
1453
+ // #2596: only written when a scope was actually declared — conservative in
1454
+ // what we send, so a blank --files leaves the 4-field shape untouched.
1455
+ if (Array.isArray(plan.entry.files_modified) && plan.entry.files_modified.length > 0) {
1456
+ recorded.files_modified = plan.entry.files_modified;
1457
+ }
1294
1458
  const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
1295
1459
  const alreadyPresent = worktrees.some((existing) => {
1296
1460
  const normalized = normalizeCleanupManifestEntry(existing);
@@ -1677,6 +1841,9 @@ module.exports = {
1677
1841
  normalizeCleanupManifest,
1678
1842
  planWorktreeWaveCleanup,
1679
1843
  executeWorktreeWaveCleanupPlan,
1844
+ WAVE_CLEANUP_WARNING,
1845
+ planWaveScopeConformance,
1846
+ isSummaryArtifactRelPath,
1680
1847
  cmdWorktreeCleanupWave,
1681
1848
  planWorktreeRecordAgent,
1682
1849
  cmdWorktreeRecordAgent,
@@ -28,6 +28,7 @@
28
28
  "verifier": true,
29
29
  "nyquist_validation": true,
30
30
  "ai_integration_phase": true,
31
+ "agent_hint_routing": true,
31
32
  "human_verify_mode": "end-of-phase",
32
33
  "auto_advance": false,
33
34
  "_auto_chain_active": false,
@@ -35,6 +35,7 @@
35
35
  "workflow.plan_chunked",
36
36
  "workflow.specless_probe_fallback",
37
37
  "workflow.plan_review_convergence",
38
+ "workflow.agent_hint_routing",
38
39
  "code_quality.fallow.enabled",
39
40
  "code_quality.fallow.scope",
40
41
  "code_quality.fallow.profile",
@@ -180,7 +181,12 @@
180
181
  {
181
182
  "topLevel": "model_policy",
182
183
  "source": "^model_policy\\.runtime_tiers\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$",
183
- "description": "model_policy.runtime_tiers.<runtime>.<opus|sonnet|haiku>"
184
+ "description": "model_policy.runtime_tiers.<runtime>.<opus|sonnet|haiku> (#3587)"
185
+ },
186
+ {
187
+ "topLevel": "phase_commit_docs",
188
+ "source": "^phase_commit_docs\\.\\d+[A-Z]?(?:\\.\\d+)*$",
189
+ "description": "phase_commit_docs.<phase-id> — per-phase commit_docs override (#3587). The <phase-id> segment is a hand-copy of the canonical PHASE_NUMBER_TOKEN_SOURCE grammar owned by src/phase-id.cts (#2128); this manifest is hand-maintained JSON so it cannot import that constant. Pinned against drift by the behavioral parity test in tests/commit-docs-bypass.test.cjs (folded 'phase-commit-docs' block, describe block 'E', #3587) — do not hand-edit this pattern without updating that test."
184
190
  }
185
191
  ]
186
192
  }