@webpresso/agent-kit 3.1.28 → 3.2.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.

Potentially problematic release.


This version of @webpresso/agent-kit might be problematic. Click here for more details.

Files changed (329) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +179 -19
  3. package/catalog/AGENTS.md.tpl +6 -6
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  6. package/catalog/agent/rules/ci-test-perf.md +5 -1
  7. package/catalog/agent/rules/cmd-execution.md +9 -0
  8. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  9. package/catalog/agent/rules/pre-implementation.md +30 -2
  10. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  11. package/catalog/agent/rules/test-scan-perf.md +80 -0
  12. package/catalog/agent/rules/typescript-version.md +50 -0
  13. package/catalog/agent/rules/workflow-skills-routing.md +4 -2
  14. package/catalog/agent/skills/codex/SKILL.md +25 -1
  15. package/catalog/agent/skills/fix/SKILL.md +3 -0
  16. package/catalog/agent/skills/verify/SKILL.md +22 -2
  17. package/catalog/docs/templates/blueprint.md +1 -1
  18. package/dist/esm/audit/agents.js +1 -0
  19. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  20. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  21. package/dist/esm/audit/blueprint-pr-coverage.js +72 -7
  22. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
  24. package/dist/esm/audit/ci-test-perf.js +30 -0
  25. package/dist/esm/audit/command-surface.js +4 -2
  26. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  27. package/dist/esm/audit/host-substitution-risk.js +256 -0
  28. package/dist/esm/audit/registry.d.ts +4 -4
  29. package/dist/esm/audit/registry.js +11 -1
  30. package/dist/esm/audit/roadmap-links.js +6 -0
  31. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  32. package/dist/esm/audit/test-scan-perf.js +275 -0
  33. package/dist/esm/audit/toolchain-isolation.js +1 -2
  34. package/dist/esm/audit/typescript-version.d.ts +2 -0
  35. package/dist/esm/audit/typescript-version.js +123 -0
  36. package/dist/esm/blueprint/core/parser.js +43 -1
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +65 -10
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  46. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  47. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  48. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  49. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  50. package/dist/esm/blueprint/lifecycle/review-provenance.js +118 -12
  51. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  52. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  53. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  54. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  55. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  56. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  57. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  58. package/dist/esm/blueprint/trust/gates.js +20 -0
  59. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  60. package/dist/esm/blueprint/trust/promotion.js +62 -112
  61. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  62. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  63. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  64. package/dist/esm/blueprint/trust/validator.js +7 -1
  65. package/dist/esm/blueprint/utils/archive.js +27 -5
  66. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  67. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  68. package/dist/esm/build/cli-mcp-parity.js +10 -0
  69. package/dist/esm/ci/act-replay.js +110 -7
  70. package/dist/esm/ci/act-runner.d.ts +4 -0
  71. package/dist/esm/ci/act-runner.js +85 -26
  72. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  73. package/dist/esm/ci/act-worktree-git.js +219 -0
  74. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  75. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  76. package/dist/esm/cli/cli.d.ts +1 -1
  77. package/dist/esm/cli/cli.js +34 -0
  78. package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
  79. package/dist/esm/cli/commands/agent-launch.js +82 -4
  80. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  81. package/dist/esm/cli/commands/audit-core.js +8 -1
  82. package/dist/esm/cli/commands/audit.js +20 -5
  83. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  84. package/dist/esm/cli/commands/blueprint/mutations.js +83 -47
  85. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  86. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  87. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  88. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  89. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  90. package/dist/esm/cli/commands/blueprint/router.js +40 -3
  91. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  92. package/dist/esm/cli/commands/ci-preflight.js +6 -3
  93. package/dist/esm/cli/commands/ci.d.ts +8 -1
  94. package/dist/esm/cli/commands/ci.js +80 -21
  95. package/dist/esm/cli/commands/compile.js +5 -31
  96. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  97. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  98. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  99. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  100. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  101. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  102. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  103. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  104. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  105. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  106. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  107. package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
  108. package/dist/esm/cli/commands/dash/plan-once.js +41 -4
  109. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  110. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  111. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  112. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  113. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  114. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  115. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  116. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  117. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  118. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  119. package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
  120. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  121. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  122. package/dist/esm/cli/commands/doctor.js +5 -0
  123. package/dist/esm/cli/commands/format.js +0 -2
  124. package/dist/esm/cli/commands/hook.d.ts +5 -0
  125. package/dist/esm/cli/commands/hook.js +15 -9
  126. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  127. package/dist/esm/cli/commands/init/config.js +3 -6
  128. package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
  129. package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
  130. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  131. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  132. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  133. package/dist/esm/cli/commands/init/convergence.js +57 -0
  134. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  135. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  136. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  137. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  138. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  139. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  140. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  141. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  142. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  143. package/dist/esm/cli/commands/init/index.d.ts +1 -0
  144. package/dist/esm/cli/commands/init/index.js +326 -173
  145. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  146. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  147. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  148. package/dist/esm/cli/commands/init/merge.js +30 -1
  149. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  150. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  151. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  152. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  153. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
  154. package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
  155. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  156. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  157. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  158. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  159. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  160. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
  161. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  162. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  163. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  164. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  165. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  166. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +297 -173
  167. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
  168. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
  169. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  170. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  171. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  172. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  173. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  174. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  175. package/dist/esm/cli/commands/logs.d.ts +15 -0
  176. package/dist/esm/cli/commands/logs.js +192 -3
  177. package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
  178. package/dist/esm/cli/commands/opencode-rotate.js +135 -0
  179. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  180. package/dist/esm/cli/commands/public-readiness.js +42 -0
  181. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  182. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  183. package/dist/esm/cli/commands/review.d.ts +35 -3
  184. package/dist/esm/cli/commands/review.js +734 -217
  185. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  186. package/dist/esm/cli/commands/run-wait.js +122 -0
  187. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  188. package/dist/esm/cli/commands/secrets.js +4 -1
  189. package/dist/esm/cli/commands/setup-help.js +1 -1
  190. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  191. package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
  192. package/dist/esm/cli/direct-provider-launch.js +31 -3
  193. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  194. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  195. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  196. package/dist/esm/daemon/domains/git-actions.js +136 -0
  197. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  198. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  199. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  200. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  201. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  202. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  203. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  204. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  205. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  206. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  207. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  208. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  209. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  210. package/dist/esm/daemon/handlers/product.js +10 -119
  211. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  212. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  213. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  214. package/dist/esm/daemon/handlers/shared.js +16 -0
  215. package/dist/esm/daemon/methods.d.ts +8 -1
  216. package/dist/esm/daemon/methods.js +7 -0
  217. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  218. package/dist/esm/daemon/protocol/contract.js +26 -0
  219. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  220. package/dist/esm/daemon/protocol/index.js +1 -1
  221. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  222. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  223. package/dist/esm/hooks/conformance/batch.js +5 -0
  224. package/dist/esm/hooks/doctor.d.ts +15 -0
  225. package/dist/esm/hooks/doctor.js +79 -8
  226. package/dist/esm/hooks/permission-request/index.js +9 -3
  227. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  228. package/dist/esm/hooks/permission-request/policy.js +13 -0
  229. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  230. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  231. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  232. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  233. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  234. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  235. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  236. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +58 -4
  237. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  238. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  239. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  240. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  241. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  242. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  243. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  244. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  245. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  246. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  247. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  248. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  249. package/dist/esm/hooks/shared/types.d.ts +8 -5
  250. package/dist/esm/hooks/shared/types.js +23 -8
  251. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  252. package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
  253. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  254. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
  255. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  256. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  257. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  258. package/dist/esm/mcp/blueprint/registration.js +3 -2
  259. package/dist/esm/mcp/server.d.ts +1 -0
  260. package/dist/esm/mcp/server.js +27 -0
  261. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  262. package/dist/esm/mcp/tools/_names.js +5 -0
  263. package/dist/esm/mcp/tools/_registry.js +10 -0
  264. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  265. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  266. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  267. package/dist/esm/mcp/tools/audit.js +40 -0
  268. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  269. package/dist/esm/mcp/tools/ci-act.js +9 -11
  270. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  271. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  272. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  273. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  274. package/dist/esm/mcp/tools/review-run.js +216 -0
  275. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  276. package/dist/esm/mcp/tools/run-wait.js +593 -0
  277. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  278. package/dist/esm/mcp/tools/session-id.js +45 -0
  279. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  280. package/dist/esm/mcp/tools/session-info.js +49 -0
  281. package/dist/esm/output-transforms/audit.d.ts +23 -0
  282. package/dist/esm/output-transforms/audit.js +47 -0
  283. package/dist/esm/output-transforms/index.js +2 -0
  284. package/dist/esm/paths/state-root.js +3 -0
  285. package/dist/esm/review/authority.js +30 -7
  286. package/dist/esm/review/availability.d.ts +22 -0
  287. package/dist/esm/review/availability.js +123 -23
  288. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  289. package/dist/esm/review/delivery-verifier.js +115 -0
  290. package/dist/esm/review/events.d.ts +3 -2
  291. package/dist/esm/review/events.js +23 -3
  292. package/dist/esm/review/execution/adapters.js +26 -9
  293. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  294. package/dist/esm/review/execution/output-policy.js +12 -0
  295. package/dist/esm/review/execution/review-checkout.js +87 -6
  296. package/dist/esm/review/execution/supervisor.js +5 -0
  297. package/dist/esm/review/execution/types.d.ts +8 -0
  298. package/dist/esm/review/opencode-account.d.ts +24 -0
  299. package/dist/esm/review/opencode-account.js +68 -0
  300. package/dist/esm/review/opencode-auth.d.ts +33 -0
  301. package/dist/esm/review/opencode-auth.js +79 -0
  302. package/dist/esm/review/subject.d.ts +27 -0
  303. package/dist/esm/review/subject.js +179 -32
  304. package/dist/esm/runtime/executor.d.ts +1 -0
  305. package/dist/esm/runtime/executor.js +2 -1
  306. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  307. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  308. package/dist/esm/secret-gate/runner.d.ts +1 -0
  309. package/dist/esm/secret-gate/runner.js +4 -1
  310. package/dist/esm/status/snapshot.js +30 -19
  311. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  312. package/dist/esm/test/duration-sequencer.js +54 -0
  313. package/dist/esm/test/shard-durations.json +922 -0
  314. package/dist/esm/test/worker-budget.d.ts +6 -0
  315. package/dist/esm/test/worker-budget.js +59 -1
  316. package/dist/esm/test-helpers/global-setup.d.ts +1 -1
  317. package/dist/esm/test-helpers/global-setup.js +68 -24
  318. package/dist/esm/ultragoal/service.d.ts +7 -0
  319. package/dist/esm/ultragoal/service.js +42 -2
  320. package/dist/esm/utils/write-json-file.js +3 -1
  321. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  322. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  323. package/dist/esm/worktrees/current.d.ts +1 -0
  324. package/dist/esm/worktrees/current.js +1 -0
  325. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  326. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  327. package/dist/esm/worktrees/registry.d.ts +8 -0
  328. package/dist/esm/worktrees/registry.js +18 -3
  329. package/package.json +18 -15
@@ -43,6 +43,7 @@ export declare function countDistinctLogBackedApprovals(file: string, approvals:
43
43
  export interface ApprovalGateEvaluation {
44
44
  distinct: number;
45
45
  openCodeGoModelReviewers: string[];
46
+ overrideReviewers: string[];
46
47
  satisfied: boolean;
47
48
  strongReviewers: string[];
48
49
  }
@@ -59,5 +60,24 @@ export declare function validateApprovalGate(file: string, frontmatter: Lifecycl
59
60
  completedApprovalGate?: boolean;
60
61
  inProgressApprovalGate?: boolean;
61
62
  }): BlueprintAuditIssue[];
63
+ /**
64
+ * Write-path mirror of {@link validateApprovalGate}'s `completed` branch.
65
+ *
66
+ * There is no single completion chokepoint: this helper is called from all
67
+ * three write-path owners that can set `status: completed` — the MCP transition
68
+ * funnel `applyLocalBlueprintTransition` (finalize/promote/transition), the CLI
69
+ * `promoteBlueprintLocked` (which does not traverse that funnel), and the
70
+ * execution engine's `finalize` intent via `applyBlueprintLifecycleToFile`
71
+ * (exec/resume/stop/move). Calling it before each write guarantees a blueprint
72
+ * can only reach `completed` when it carries the same plan-purpose approval CI's
73
+ * `wp audit blueprint-lifecycle` requires afterward. It delegates to
74
+ * {@link validateApprovalGate} verbatim — including the pre-rollout grandfather
75
+ * ({@link predatesCompletedApprovalGate}) and the v2 `review-events.jsonl`
76
+ * ledger — so the write-path and the audit-path can never diverge and a
77
+ * finalized blueprint can never fail its own lifecycle audit.
78
+ *
79
+ * @throws Error when no plan-purpose approval satisfies the promotion reviewer mix.
80
+ */
81
+ export declare function assertCompletedApprovalGate(file: string, markdown: string): void;
62
82
  export declare function runBlueprintAudit(options: RunBlueprintAuditOptions): Promise<BlueprintAuditResult>;
63
83
  export {};
@@ -13,6 +13,7 @@ import { resolveBlueprintRoot } from "#utils/blueprint-root";
13
13
  import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
14
14
  import { relativeBlueprintSlug } from "#lifecycle/local.js";
15
15
  import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, } from "#lifecycle/review-provenance.js";
16
+ import { MAINTAINER_OVERRIDE_REVIEWER } from "#review/events.js";
16
17
  import { isPolicyApprovalVerdict } from "#review/verdict.js";
17
18
  function isBlueprintOverview(file) {
18
19
  const normalized = file.replace(/\\/g, "/");
@@ -94,11 +95,13 @@ function approvalReviewerKey(reviewer) {
94
95
  return parts.at(-1) ?? normalized;
95
96
  }
96
97
  function provenanceBackedApprovalReviewers(file, approvals, options = {}) {
97
- const purpose = options.purpose ?? "plan";
98
- const legacy = purpose === "plan"
98
+ const purposes = options.purpose
99
+ ? [options.purpose]
100
+ : ["plan", "delivery"];
101
+ const legacy = purposes.includes("plan")
99
102
  ? collectTrackedReviewProvenance(file, approvals, options).map((record) => record.reviewer)
100
103
  : [];
101
- const current = collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer);
104
+ const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
102
105
  return new Set([...legacy, ...current]);
103
106
  }
104
107
  export function countDistinctProvenanceBackedApprovals(file, approvals) {
@@ -116,15 +119,24 @@ export function evaluateApprovalReviewers(reviewerIds) {
116
119
  const openCodeGoModelReviewers = reviewers
117
120
  .map(approvalReviewerKey)
118
121
  .filter((reviewer) => OPENCODE_GO_MODEL_APPROVAL_REVIEWERS.has(reviewer));
122
+ const overrideReviewers = reviewers
123
+ .map(approvalReviewerKey)
124
+ .filter((reviewer) => reviewer === MAINTAINER_OVERRIDE_REVIEWER);
119
125
  return {
120
126
  distinct: reviewers.length,
121
127
  openCodeGoModelReviewers: [...new Set(openCodeGoModelReviewers)].sort(),
122
- satisfied: strongReviewers.length >= 1 || new Set(openCodeGoModelReviewers).size >= 1,
128
+ overrideReviewers: [...new Set(overrideReviewers)].sort(),
129
+ satisfied: strongReviewers.length >= 1 ||
130
+ new Set(openCodeGoModelReviewers).size >= 1 ||
131
+ new Set(overrideReviewers).size >= 1,
123
132
  strongReviewers: [...new Set(strongReviewers)].sort(),
124
133
  };
125
134
  }
126
135
  export function formatApprovalGateRequirement(evaluation) {
127
- return `need 1 provenance-backed Claude/Codex/Grok or OpenCode-Go model approval; currently have ${evaluation.strongReviewers.length} Claude/Codex/Grok approval(s), ${evaluation.openCodeGoModelReviewers.length} OpenCode-Go model approval(s), ${evaluation.distinct} distinct provenance-backed approval(s) total`;
136
+ const override = evaluation.overrideReviewers.length >= 1
137
+ ? ` + ${evaluation.overrideReviewers.length} maintainer-override(s)`
138
+ : "";
139
+ return `need 1 provenance-backed Claude/Codex/Grok or OpenCode-Go model approval (or a recorded maintainer-override); currently have ${evaluation.strongReviewers.length} Claude/Codex/Grok approval(s), ${evaluation.openCodeGoModelReviewers.length} OpenCode-Go model approval(s)${override}, ${evaluation.distinct} distinct provenance-backed approval(s) total`;
128
140
  }
129
141
  export function validateApprovalGate(file, frontmatter, options = {}) {
130
142
  if (frontmatter.type !== "blueprint")
@@ -162,6 +174,31 @@ export function validateApprovalGate(file, frontmatter, options = {}) {
162
174
  message: `Review evidence integrity: ${issue}`,
163
175
  }));
164
176
  }
177
+ /**
178
+ * Write-path mirror of {@link validateApprovalGate}'s `completed` branch.
179
+ *
180
+ * There is no single completion chokepoint: this helper is called from all
181
+ * three write-path owners that can set `status: completed` — the MCP transition
182
+ * funnel `applyLocalBlueprintTransition` (finalize/promote/transition), the CLI
183
+ * `promoteBlueprintLocked` (which does not traverse that funnel), and the
184
+ * execution engine's `finalize` intent via `applyBlueprintLifecycleToFile`
185
+ * (exec/resume/stop/move). Calling it before each write guarantees a blueprint
186
+ * can only reach `completed` when it carries the same plan-purpose approval CI's
187
+ * `wp audit blueprint-lifecycle` requires afterward. It delegates to
188
+ * {@link validateApprovalGate} verbatim — including the pre-rollout grandfather
189
+ * ({@link predatesCompletedApprovalGate}) and the v2 `review-events.jsonl`
190
+ * ledger — so the write-path and the audit-path can never diverge and a
191
+ * finalized blueprint can never fail its own lifecycle audit.
192
+ *
193
+ * @throws Error when no plan-purpose approval satisfies the promotion reviewer mix.
194
+ */
195
+ export function assertCompletedApprovalGate(file, markdown) {
196
+ const frontmatter = readLifecycleAuditFrontmatter(markdown);
197
+ const errors = validateApprovalGate(file, { ...frontmatter, status: "completed" }, { completedApprovalGate: true }).filter((issue) => issue.level === "error");
198
+ if (errors.length > 0) {
199
+ throw new Error(errors.map((issue) => issue.message).join("; "));
200
+ }
201
+ }
165
202
  function countTaskHeadings(raw) {
166
203
  return raw.match(/^####\s+(?:\[[^\]]+\]\s+)?Task\s+/gm)?.length ?? 0;
167
204
  }
@@ -226,7 +263,13 @@ function validateTaskState(blueprint) {
226
263
  }
227
264
  return issues;
228
265
  }
229
- function validateBlueprintSlugUniqueness(blueprints) {
266
+ function validateBlueprintSlugUniqueness(blueprints,
267
+ // When auditing a changed-only subset, `stagedSet` restricts reporting to
268
+ // duplicates a CHANGED blueprint participates in, and attributes the error to
269
+ // that changed file so a scope-filtering caller still surfaces it. A
270
+ // pre-existing duplicate among only-untouched blueprints is left for the
271
+ // full-corpus (CI) pass, not the changed-only hook.
272
+ scope) {
230
273
  const grouped = new Map();
231
274
  for (const blueprint of blueprints) {
232
275
  const normalizedSlug = relativeBlueprintSlug(blueprint.slug);
@@ -239,12 +282,18 @@ function validateBlueprintSlugUniqueness(blueprints) {
239
282
  }
240
283
  }
241
284
  const issues = [];
285
+ const stagedSet = scope?.stagedSet ?? null;
242
286
  for (const [normalizedSlug, entries] of grouped) {
243
287
  const uniqueLifecycleSlugs = new Set(entries.map((entry) => entry.slug));
244
288
  if (uniqueLifecycleSlugs.size <= 1)
245
289
  continue;
290
+ const changedEntry = stagedSet
291
+ ? entries.find((entry) => stagedSet.has(normalizePath(path.relative(scope.projectRoot, entry.file))))
292
+ : undefined;
293
+ if (stagedSet && !changedEntry)
294
+ continue;
246
295
  issues.push({
247
- file: entries[0]?.file,
296
+ file: (changedEntry ?? entries[0])?.file,
248
297
  level: "error",
249
298
  message: `Blueprint slug "${normalizedSlug}" appears in multiple lifecycle locations: ${Array.from(uniqueLifecycleSlugs)
250
299
  .toSorted()
@@ -620,10 +669,16 @@ export async function runBlueprintAudit(options) {
620
669
  baseDir: resolveBlueprintRoot(options.projectRoot),
621
670
  includeSpecialFolders: true,
622
671
  });
623
- const blueprintFiles = options.all || !options.stagedFiles
672
+ const stagedSet = options.all || !options.stagedFiles ? null : new Set(options.stagedFiles.map(normalizePath));
673
+ const blueprintFiles = stagedSet === null
624
674
  ? scanned
625
- : scanned.filter((entry) => new Set(options.stagedFiles?.map(normalizePath) ?? []).has(normalizePath(path.relative(options.projectRoot, entry.path))));
626
- issues.push(...validateBlueprintSlugUniqueness(blueprintFiles));
675
+ : scanned.filter((entry) => stagedSet.has(normalizePath(path.relative(options.projectRoot, entry.path))));
676
+ // Slug uniqueness is a corpus-wide invariant, so validate it over ALL scanned
677
+ // blueprints even in changed-only mode — otherwise a changed blueprint that
678
+ // duplicates an *untouched* lifecycle slug would only be compared with itself
679
+ // and the error would vanish. Only the expensive per-file audit below is
680
+ // scoped to the changed subset.
681
+ issues.push(...validateBlueprintSlugUniqueness(scanned, { projectRoot: options.projectRoot, stagedSet }));
627
682
  for (const entry of blueprintFiles) {
628
683
  issues.push(...(await auditBlueprintFile(entry.path, entry.slug, options)));
629
684
  }
@@ -0,0 +1,6 @@
1
+ export interface CanonicalizeBlueprintResult {
2
+ readonly from: string;
3
+ readonly to: string;
4
+ readonly moved: boolean;
5
+ }
6
+ export declare function canonicalizeBlueprint(projectRoot: string, slug: string): Promise<CanonicalizeBlueprintResult>;
@@ -0,0 +1,135 @@
1
+ import { constants } from "node:fs";
2
+ import { access, lstat, mkdir, readdir, rename, rmdir } from "node:fs/promises";
3
+ import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
4
+ import { resolveBlueprintFile } from "#lifecycle/local.js";
5
+ import { BLUEPRINT_STATUSES, getBlueprintDocumentPaths, isBlueprintSlugSegment, isBlueprintStatus, } from "#utils/document-paths.js";
6
+ import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
7
+ async function exists(filePath) {
8
+ try {
9
+ await access(filePath, constants.F_OK);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function assertRegularFile(filePath) {
17
+ const stats = await lstat(filePath);
18
+ if (!stats.isFile() || stats.isSymbolicLink()) {
19
+ throw new Error(`Blueprint canonicalization requires a regular file: ${filePath}`);
20
+ }
21
+ }
22
+ async function directCanonicalizeLocation(projectRoot, slug) {
23
+ const root = resolveBlueprintRoot(projectRoot);
24
+ const segments = slug.split("/").filter((segment) => segment.length > 0);
25
+ const scopedState = isBlueprintStatus(segments[0]) ? segments[0] : null;
26
+ const slugSegments = scopedState === null ? segments : segments.slice(1);
27
+ const stableSlug = slugSegments.join("/");
28
+ // Validate before any filesystem probe: an unvalidated slug such as
29
+ // "../outside" would otherwise resolve paths outside the blueprint root.
30
+ if (!stableSlug || !slugSegments.every((segment) => isBlueprintSlugSegment(segment))) {
31
+ return null;
32
+ }
33
+ const states = scopedState === null ? BLUEPRINT_STATUSES : [scopedState];
34
+ for (const state of states) {
35
+ const paths = getBlueprintDocumentPaths(root, state, stableSlug);
36
+ const [flatExists, folderExists] = await Promise.all([
37
+ exists(paths.flat),
38
+ exists(paths.folder),
39
+ ]);
40
+ if (flatExists && folderExists) {
41
+ await assertRegularFile(paths.flat);
42
+ await assertRegularFile(paths.folder);
43
+ throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${paths.folder}`);
44
+ }
45
+ if (flatExists) {
46
+ return {
47
+ folderDirectory: paths.directory,
48
+ folderPath: paths.folder,
49
+ path: paths.flat,
50
+ shape: "flat",
51
+ };
52
+ }
53
+ if (folderExists) {
54
+ return {
55
+ folderDirectory: paths.directory,
56
+ folderPath: paths.folder,
57
+ path: paths.folder,
58
+ shape: "folder",
59
+ };
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ const GOVERNANCE_ENTRY_NAMES = new Set(["review-events.jsonl", "reviews.md", "review-artifacts"]);
65
+ async function assertUsableTargetDirectory(directory) {
66
+ try {
67
+ const stats = await lstat(directory);
68
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
69
+ throw new Error(`Blueprint canonicalization target is not a real directory: ${directory}`);
70
+ }
71
+ }
72
+ catch (error) {
73
+ const code = error.code;
74
+ if (code !== "ENOENT")
75
+ throw error;
76
+ await mkdir(directory, { recursive: true });
77
+ return "created";
78
+ }
79
+ // Adopting a directory that already holds governance files would turn
80
+ // previously unattributed evidence into authoritative folder evidence the
81
+ // moment the overview lands next to it. Refuse; the operator must review
82
+ // and remove (or intentionally commit) that evidence first. Compare
83
+ // case-folded: on case-insensitive filesystems "Reviews.md" aliases the
84
+ // authoritative lowercase path.
85
+ const adopted = (await readdir(directory)).filter((entry) => GOVERNANCE_ENTRY_NAMES.has(entry.toLowerCase()));
86
+ if (adopted.length > 0) {
87
+ throw new Error(`Blueprint canonicalization refused to adopt a directory containing review evidence: ${directory} (${adopted.join(", ")})`);
88
+ }
89
+ return "existing";
90
+ }
91
+ async function removeCreatedDirectoryIfEmpty(directory, created) {
92
+ if (!created)
93
+ return;
94
+ try {
95
+ const entries = await readdir(directory);
96
+ if (entries.length === 0)
97
+ await rmdir(directory);
98
+ }
99
+ catch {
100
+ // Best-effort cleanup only. A concurrent caller may have completed the move.
101
+ }
102
+ }
103
+ export async function canonicalizeBlueprint(projectRoot, slug) {
104
+ const location = (await directCanonicalizeLocation(projectRoot, slug)) ??
105
+ (await resolveBlueprintFile(projectRoot, slug));
106
+ const from = location.path;
107
+ const to = location.folderPath;
108
+ if (location.shape === "folder") {
109
+ await assertRegularFile(to);
110
+ return { from, to, moved: false };
111
+ }
112
+ await assertRegularFile(from);
113
+ const directoryState = await assertUsableTargetDirectory(location.folderDirectory);
114
+ const createdDirectory = directoryState === "created";
115
+ try {
116
+ if (await exists(to)) {
117
+ await assertRegularFile(to);
118
+ if (await exists(from)) {
119
+ throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${to}`);
120
+ }
121
+ return { from, to, moved: false };
122
+ }
123
+ await rename(from, to);
124
+ }
125
+ catch (error) {
126
+ if (error.code === "ENOENT" && (await exists(to))) {
127
+ await assertRegularFile(to);
128
+ return { from, to, moved: false };
129
+ }
130
+ await removeCreatedDirectoryIfEmpty(location.folderDirectory, createdDirectory);
131
+ throw error;
132
+ }
133
+ refreshBlueprintReadmeIndex(projectRoot);
134
+ return { from, to, moved: true };
135
+ }
@@ -35,5 +35,14 @@ export interface BlueprintLifecycleResult {
35
35
  progress: string;
36
36
  targetStatus: LifecycleBlueprintStatus;
37
37
  }
38
+ /**
39
+ * Update only the named frontmatter keys, byte-preserving every other line.
40
+ * Review authority binds plan approvals to normalized blueprint bytes
41
+ * (`normalizePlanSubjectContent`), so lifecycle writers must never re-serialize
42
+ * fields they do not own — a whole-document YAML round-trip (quote-style or
43
+ * folding changes to an untouched `title:`) would strand committed approvals
44
+ * mid-lifecycle. Falls back to a gray-matter round-trip only when the document
45
+ * has no frontmatter block at all.
46
+ */
38
47
  export declare function setBlueprintFrontmatterFields(markdown: string, updates: Record<string, string | string[] | undefined>): string;
39
48
  export declare function applyBlueprintLifecycle(markdown: string, slug: string, intent: BlueprintLifecycleIntent): BlueprintLifecycleResult;
@@ -1,4 +1,5 @@
1
1
  import matter from "gray-matter";
2
+ import { stringify as stringifyYaml } from "yaml";
2
3
  import { parseBlueprint } from "#core/parser";
3
4
  import { lifecycleBlueprintStatusSchema } from "#core/schema";
4
5
  import { updateBlockedReason, updateTaskStatus } from "#markdown/helpers";
@@ -6,17 +7,63 @@ import { applyVerification, assertAllTasksHaveCanonicalPassingEvidence } from "#
6
7
  function todayIsoDate() {
7
8
  return new Date().toISOString().split("T")[0] ?? new Date().toISOString();
8
9
  }
10
+ function serializeFrontmatterEntry(key, value) {
11
+ // lineWidth: 0 disables folding so long scalars stay on one line. gray-matter
12
+ // parses frontmatter with js-yaml, which resolves plain date-like scalars
13
+ // ("2026-07-21") into Date objects — quote a string whenever gray-matter
14
+ // would not round-trip its plain form back to the same string.
15
+ const plain = stringifyYaml({ [key]: value }, { lineWidth: 0 }).trimEnd();
16
+ if (typeof value === "string" && matter(`---\n${plain}\n---\n`).data[key] !== value) {
17
+ return stringifyYaml({ [key]: value }, { defaultKeyType: "PLAIN", defaultStringType: "QUOTE_DOUBLE", lineWidth: 0 })
18
+ .trimEnd()
19
+ .split("\n");
20
+ }
21
+ return plain.split("\n");
22
+ }
23
+ /**
24
+ * Update only the named frontmatter keys, byte-preserving every other line.
25
+ * Review authority binds plan approvals to normalized blueprint bytes
26
+ * (`normalizePlanSubjectContent`), so lifecycle writers must never re-serialize
27
+ * fields they do not own — a whole-document YAML round-trip (quote-style or
28
+ * folding changes to an untouched `title:`) would strand committed approvals
29
+ * mid-lifecycle. Falls back to a gray-matter round-trip only when the document
30
+ * has no frontmatter block at all.
31
+ */
9
32
  export function setBlueprintFrontmatterFields(markdown, updates) {
10
- const parsed = matter(markdown);
33
+ const match = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/u.exec(markdown);
34
+ if (!match) {
35
+ const parsed = matter(markdown);
36
+ for (const [key, value] of Object.entries(updates)) {
37
+ if (value === undefined || value === "") {
38
+ delete parsed.data[key];
39
+ }
40
+ else {
41
+ parsed.data[key] = value;
42
+ }
43
+ }
44
+ return matter.stringify(parsed.content, parsed.data);
45
+ }
46
+ const lines = (match[2] ?? "").split("\n");
11
47
  for (const [key, value] of Object.entries(updates)) {
12
- if (value === undefined || value === "") {
13
- delete parsed.data[key];
48
+ const keyPattern = new RegExp(String.raw `^${key}:`, "u");
49
+ const start = lines.findIndex((line) => keyPattern.test(line));
50
+ let removeCount = 0;
51
+ if (start !== -1) {
52
+ removeCount = 1;
53
+ while (start + removeCount < lines.length &&
54
+ /^[ \t]/u.test(lines[start + removeCount] ?? "")) {
55
+ removeCount += 1;
56
+ }
57
+ }
58
+ const replacement = value === undefined || value === "" ? [] : serializeFrontmatterEntry(key, value);
59
+ if (start !== -1) {
60
+ lines.splice(start, removeCount, ...replacement);
14
61
  }
15
- else {
16
- parsed.data[key] = value;
62
+ else if (replacement.length > 0) {
63
+ lines.push(...replacement);
17
64
  }
18
65
  }
19
- return matter.stringify(parsed.content, parsed.data);
66
+ return `${match[1]}${lines.join("\n")}${match[3]}${markdown.slice(match[0].length)}`;
20
67
  }
21
68
  function assertExecutableStatus(status) {
22
69
  const parsed = lifecycleBlueprintStatusSchema.safeParse(status);
@@ -1,5 +1,6 @@
1
1
  import type { BlueprintLifecycleIntent, BlueprintLifecycleResult } from "./engine.js";
2
- export interface ResolvedBlueprintFile {
2
+ import { type BlueprintDocumentIdentity } from "#utils/document-paths.js";
3
+ export interface ResolvedBlueprintFile extends BlueprintDocumentIdentity {
3
4
  path: string;
4
5
  slug: string;
5
6
  }
@@ -1,10 +1,11 @@
1
1
  import { mkdir, readFile, readdir, rename, rmdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
4
+ import { assertCompletedApprovalGate } from "#lifecycle/audit";
4
5
  import { applyBlueprintLifecycle } from "#lifecycle/engine";
5
6
  import { scanBlueprintDirectory } from "#service/scanner";
6
7
  import { resolveBlueprintRoot } from "#utils/blueprint-root";
7
- import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
8
+ import { getBlueprintDocumentPaths, resolveBlueprintDocumentIdentity, } from "#utils/document-paths.js";
8
9
  const BLUEPRINT_SLUG_SEGMENT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
10
  function isStatusSegment(segment) {
10
11
  return (segment === "draft" ||
@@ -57,7 +58,15 @@ export async function resolveBlueprintFile(projectRoot, slug) {
57
58
  .map((entry) => entry.slug)
58
59
  .join(", ")}`);
59
60
  }
60
- return { path: match.path, slug: match.slug };
61
+ const identity = resolveBlueprintDocumentIdentity(baseDir, match.path);
62
+ if (!identity) {
63
+ throw new Error(`Blueprint ${match.slug} resolved to an invalid document path: ${match.path}`);
64
+ }
65
+ return {
66
+ ...identity,
67
+ path: identity.documentPath,
68
+ slug: match.slug,
69
+ };
61
70
  }
62
71
  async function tryRemoveEmptyParent(dir) {
63
72
  try {
@@ -75,8 +84,15 @@ export async function applyBlueprintLifecycleToFile(projectRoot, slug, intent) {
75
84
  const location = await resolveBlueprintFile(projectRoot, slug);
76
85
  const raw = await readFile(location.path, "utf-8");
77
86
  const mutation = applyBlueprintLifecycle(raw, location.slug, intent);
87
+ // Completion parity: the execution engine's `finalize` intent (exec/resume/
88
+ // stop/move flows) also writes `status: completed`. Gate it with the same
89
+ // plan-approval check CI enforces so this path cannot diverge from the audit
90
+ // either — matching the MCP funnel and CLI completion owners.
91
+ if (mutation.targetStatus === "completed") {
92
+ assertCompletedApprovalGate(location.path, raw);
93
+ }
78
94
  const relativeSlug = relativeBlueprintSlug(location.slug);
79
- const isFlatFile = path.basename(location.path) !== "_overview.md";
95
+ const isFlatFile = location.shape === "flat";
80
96
  const sourceDir = path.dirname(location.path);
81
97
  const targetDocumentPaths = getBlueprintDocumentPaths(baseDir, mutation.targetStatus, relativeSlug);
82
98
  const targetDir = targetDocumentPaths.directory;
@@ -36,6 +36,12 @@ export interface TrackedReviewProvenanceOptions {
36
36
  readonly targetHash?: string;
37
37
  }
38
38
  export declare function normalizeTrackedReviewApprovals(approvals: unknown): TrackedReviewApproval[];
39
+ /**
40
+ * Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
41
+ * if missing (lifecycle move draft→planned before the promote commit), resolve
42
+ * by stable slug under any lifecycle directory at that ref.
43
+ */
44
+ export declare function readBlueprintOverviewAtRef(repoRoot: string, ref: string, file: string): Buffer | null;
39
45
  export declare function collectV2ReviewProvenance(file: string, purpose: ReviewEvent["purpose"], ref?: string, issues?: string[]): readonly ValidTrackedReview[];
40
46
  export declare function auditReviewEventAppendOnly(cwd: string, baseRef: string, targetRef?: string): readonly string[];
41
47
  export declare function parseTrackedReviewRecords(markdown: string): TrackedReviewRecord[];
@@ -1,9 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { readReviewAuthorityAtRef } from "#review/authority.js";
5
- import { parseReviewEventLog } from "#review/events.js";
6
- import { normalizePlanSubjectContent } from "#review/subject.js";
4
+ import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
5
+ import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
6
+ import { createDeliverySubjectAtRef, createPlanSubjectDigestForContent, normalizePlanSubjectContent, } from "#review/subject.js";
7
7
  import { isPolicyApprovalVerdict } from "#review/verdict.js";
8
8
  const REVIEW_ENTRY_MARKER = "<!-- wp:review-entry ";
9
9
  function normalizePath(file) {
@@ -50,8 +50,59 @@ function stableSlugForBlueprintFile(file) {
50
50
  if (lifecycleIndex >= 0 && segments.at(-1) === "_overview.md") {
51
51
  return segments.slice(lifecycleIndex + 1, -1).join("/");
52
52
  }
53
+ const basename = segments.at(-1) ?? "";
54
+ if (lifecycleIndex >= 0 && basename.endsWith(".md")) {
55
+ return basename.slice(0, -3);
56
+ }
53
57
  return path.basename(path.dirname(file));
54
58
  }
59
+ /**
60
+ * Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
61
+ * if missing (lifecycle move draft→planned before the promote commit), resolve
62
+ * by stable slug under any lifecycle directory at that ref.
63
+ */
64
+ export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
65
+ const relative = normalizePath(path.relative(repoRoot, file));
66
+ if (relative.startsWith("..") || path.isAbsolute(relative))
67
+ return null;
68
+ try {
69
+ return execFileSync("git", ["show", `${ref}:${relative}`], {
70
+ cwd: repoRoot,
71
+ encoding: "buffer",
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ timeout: 5_000,
74
+ });
75
+ }
76
+ catch {
77
+ // Path may have moved across lifecycle dirs (draft→planned) while the
78
+ // promote commit is still uncommitted. Stable slug is the authority key.
79
+ const slug = stableSlugForBlueprintFile(file);
80
+ if (!slug || slug.includes(".."))
81
+ return null;
82
+ try {
83
+ const names = execFileSync("git", ["ls-tree", "-r", "--name-only", ref], {
84
+ cwd: repoRoot,
85
+ encoding: "utf8",
86
+ stdio: ["ignore", "pipe", "pipe"],
87
+ timeout: 5_000,
88
+ });
89
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
90
+ const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}(?:/_overview\\.md|\\.md)$`, "u");
91
+ const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
92
+ if (hits.length !== 1)
93
+ return null;
94
+ return execFileSync("git", ["show", `${ref}:${hits[0]}`], {
95
+ cwd: repoRoot,
96
+ encoding: "buffer",
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ timeout: 5_000,
99
+ });
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ }
55
106
  function workingContentMatchesRef(repoRoot, file, purpose, ref) {
56
107
  const relative = path.relative(repoRoot, file);
57
108
  if (relative.startsWith("..") || path.isAbsolute(relative))
@@ -63,12 +114,11 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
63
114
  // in the working tree before the promotion commit exists, and that
64
115
  // window must not strip a legitimately promoted blueprint of its plan
65
116
  // authority. Substantive edits still differ after normalization.
66
- const atRef = execFileSync("git", ["show", `${ref}:${normalizePath(relative)}`], {
67
- cwd: repoRoot,
68
- encoding: "buffer",
69
- stdio: ["ignore", "pipe", "pipe"],
70
- timeout: 5_000,
71
- });
117
+ // Also resolve by stable slug when the lifecycle path changed (draft→planned)
118
+ // so pre-commit on a rename does not falsely drop plan authority.
119
+ const atRef = readBlueprintOverviewAtRef(repoRoot, ref, file);
120
+ if (!atRef)
121
+ return false;
72
122
  const working = readFileSync(file);
73
123
  return normalizePlanSubjectContent(working).equals(normalizePlanSubjectContent(atRef));
74
124
  }
@@ -84,27 +134,83 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
84
134
  return false;
85
135
  }
86
136
  }
137
+ // Working-ledger acceptance: pre-commit validates a blueprint whose new review
138
+ // events are staged alongside it and therefore not yet at any git ref. An
139
+ // event still counts only when its subjectDigest matches the digest of the
140
+ // exact working bytes being committed and its artifact hash-verifies on disk —
141
+ // content-addressing keeps this sound before the commit exists.
142
+ function collectWorkingV2ReviewProvenance(file, purpose, issues) {
143
+ try {
144
+ const repoRoot = findGitRoot(file);
145
+ if (!repoRoot)
146
+ return [];
147
+ const slug = stableSlugForBlueprintFile(file);
148
+ const working = readWorkingReviewEvents(file, slug);
149
+ if (working.events.length === 0)
150
+ return [];
151
+ issues?.push(...working.issues);
152
+ const subjects = purpose === "plan"
153
+ ? [
154
+ {
155
+ scheme: "wp-blueprint-v1",
156
+ digest: createPlanSubjectDigestForContent(slug, readFileSync(file)),
157
+ },
158
+ ]
159
+ : [
160
+ ...new Map(working.events
161
+ .filter((event) => event.purpose === "delivery")
162
+ .flatMap((event) => {
163
+ const subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, normalizePath(path.relative(repoRoot, file)));
164
+ if (subject.digest !== event.subjectDigest) {
165
+ issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
166
+ return [];
167
+ }
168
+ return [[subject.digest, subject]];
169
+ })).values(),
170
+ ];
171
+ return subjects
172
+ .flatMap((subject) => evaluateReviewEvents(working.events, {
173
+ purpose,
174
+ blueprintSlug: slug,
175
+ subjectScheme: subject.scheme,
176
+ subjectDigest: subject.digest,
177
+ }, working.compromisedIds).approvals)
178
+ .filter((event) => event.artifactPath)
179
+ .map((event) => ({
180
+ reviewer: event.reviewer,
181
+ artifact: event.artifactPath,
182
+ targetHash: event.subjectDigest,
183
+ }));
184
+ }
185
+ catch (error) {
186
+ issues?.push(`Working review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
187
+ return [];
188
+ }
189
+ }
87
190
  export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
88
191
  const repoRoot = findGitRoot(file);
89
192
  if (!repoRoot)
90
193
  return [];
194
+ const workingApprovals = collectWorkingV2ReviewProvenance(file, purpose, issues);
91
195
  if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
92
- return [];
196
+ return workingApprovals;
93
197
  try {
94
198
  const authority = readReviewAuthorityAtRef(repoRoot, ref, stableSlugForBlueprintFile(file), purpose);
95
199
  issues?.push(...authority.issues);
96
- return authority.approvals.map((event) => ({
200
+ const refApprovals = authority.approvals.map((event) => ({
97
201
  reviewer: event.reviewer,
98
202
  artifact: event.artifactPath,
99
203
  targetHash: event.subjectDigest,
100
204
  }));
205
+ const seen = new Set(refApprovals.map((approval) => approval.reviewer));
206
+ return [...refApprovals, ...workingApprovals.filter((entry) => !seen.has(entry.reviewer))];
101
207
  }
102
208
  catch (error) {
103
209
  // Container-level failure (unparseable ledger, duplicate ids, cross-slug
104
210
  // events, unresolved commit): fail closed for the whole file, but surface
105
211
  // the reason instead of silently reporting zero authority.
106
212
  issues?.push(`Review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
107
- return [];
213
+ return workingApprovals;
108
214
  }
109
215
  }
110
216
  function reviewEventLogsAtRef(cwd, ref) {