@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
@@ -63,8 +63,9 @@ export type ClaudeCodexDenyEnvelope = {
63
63
  readonly hookSpecificOutput: ClaudeDenyHookSpecificOutput;
64
64
  };
65
65
  /**
66
- * Dual shape for Grok freeload/native: Grok honors decision:"deny" (+ exit 2);
67
- * Claude still reads hookSpecificOutput.
66
+ * Dual shape for native/unlabeled Grok: Grok honors decision:"deny" (+ exit 2).
67
+ * Claude Code / Codex reject a top-level `decision` other than approve|block,
68
+ * so they never receive this shape — they read hookSpecificOutput instead.
68
69
  */
69
70
  export type GrokDualDenyEnvelope = {
70
71
  readonly decision: "deny";
@@ -75,9 +76,11 @@ export type DenyEnvelope = ClaudeCodexDenyEnvelope | GrokDualDenyEnvelope;
75
76
  /**
76
77
  * Build a policy deny envelope.
77
78
  *
78
- * - `codex`: Claude-compatible hookSpecificOutput only (golden schema).
79
- * - `grok` / default (incl. freeload with host labeled claude): dual Grok
80
- * decision:"deny" + Claude hookSpecificOutput. Callers must also exit 2.
79
+ * - `claude` / `codex`: hookSpecificOutput only, because Claude Code and Codex
80
+ * strict-validate the hook-output root and reject any top-level `decision`
81
+ * other than approve|block. Deny is still delivered via the caller's exit 2.
82
+ * - `grok` / default / unknown: dual Grok decision:"deny" + hookSpecificOutput
83
+ * (Grok reads the top-level decision). Callers must also exit 2.
81
84
  */
82
85
  export declare function buildDenyEnvelope(options: {
83
86
  readonly reason: string;
@@ -1,3 +1,4 @@
1
+ import { truncateReasonToBudget } from "#hooks/shared/guard-outcome";
1
2
  /**
2
3
  * Prefer snake_case when both styles are present (Claude/Codex contract).
3
4
  * Accept Grok camelCase envelopes so freeload and native Grok hooks parse.
@@ -148,25 +149,39 @@ function resolveDenyHost(env = process.env) {
148
149
  /**
149
150
  * Build a policy deny envelope.
150
151
  *
151
- * - `codex`: Claude-compatible hookSpecificOutput only (golden schema).
152
- * - `grok` / default (incl. freeload with host labeled claude): dual Grok
153
- * decision:"deny" + Claude hookSpecificOutput. Callers must also exit 2.
152
+ * - `claude` / `codex`: hookSpecificOutput only, because Claude Code and Codex
153
+ * strict-validate the hook-output root and reject any top-level `decision`
154
+ * other than approve|block. Deny is still delivered via the caller's exit 2.
155
+ * - `grok` / default / unknown: dual Grok decision:"deny" + hookSpecificOutput
156
+ * (Grok reads the top-level decision). Callers must also exit 2.
154
157
  */
155
158
  export function buildDenyEnvelope(options, env = process.env) {
159
+ // Defensive budget: the permissionDecisionReason contract is <=80 chars. A
160
+ // reason from formatGuardReason is already within budget (idempotent here);
161
+ // a raw over-budget reason from any future caller is truncated by Unicode
162
+ // code points (never splitting a surrogate pair, matching formatGuardReason),
163
+ // from the end, so a leading GUARD_ code prefix always survives.
164
+ const reason = truncateReasonToBudget(options.reason);
156
165
  const hookSpecificOutput = {
157
166
  hookEventName: "PreToolUse",
158
167
  permissionDecision: "deny",
159
- permissionDecisionReason: options.reason,
168
+ permissionDecisionReason: reason,
160
169
  };
170
+ // Claude Code and Codex strict-validate the PreToolUse hook-output root and
171
+ // REJECT any top-level `decision` other than approve|block (Claude Code
172
+ // surfaces "Hook JSON output validation failed"). A real Claude host is
173
+ // labeled WP_HOOK_HOST=claude, so it MUST get the schema-valid
174
+ // hookSpecificOutput shape — not the dual envelope. The runner's exit-2
175
+ // delivers the deny regardless of stdout shape.
161
176
  const host = resolveDenyHost(env);
162
- if (host === "codex") {
177
+ if (host === "codex" || host === "claude") {
163
178
  return { hookSpecificOutput };
164
179
  }
165
- // Grok native, freeload (often WP_HOOK_HOST=claude), and unknown hosts:
166
- // dual-emit so Grok JSON deny works; Claude ignores extra top-level keys.
180
+ // Native Grok (WP_HOOK_HOST=grok) and unknown/unlabeled hosts (treated as a
181
+ // Grok freeload fallback): dual-emit so Grok's JSON deny works. Callers exit 2.
167
182
  return {
168
183
  decision: "deny",
169
- reason: options.reason,
184
+ reason,
170
185
  hookSpecificOutput,
171
186
  };
172
187
  }
@@ -21,6 +21,17 @@ export declare function getChangedFiles(projectDir: string): string[];
21
21
  export declare function filterQaFiles(files: string[]): string[];
22
22
  export declare function getTypecheckFiles(files: string[]): string[];
23
23
  export declare function findTestFiles(sourceFile: string, projectDir: string): string[];
24
+ /**
25
+ * Every test path the cross-cutting map can yield.
26
+ *
27
+ * Exported so a guard test can assert these still exist on disk. Without that
28
+ * guard a rename would keep the unit tests green — they only compare strings —
29
+ * while the caller's existence filter silently dropped the path, reinstating
30
+ * the very under-selection this map exists to prevent.
31
+ */
32
+ export declare function crossCuttingTestDependentPaths(): string[];
33
+ /** Non-colocated tests a changed file is known to affect. */
34
+ export declare function findCrossCuttingTestFiles(sourceFile: string): string[];
24
35
  export declare function discoverTestFiles(changedFiles: string[], projectDir: string): string[];
25
36
  export declare function buildTypecheckCommand(files: string[]): string | null;
26
37
  export declare function buildTestCommand(files: string[]): string | null;
@@ -54,11 +54,85 @@ export function findTestFiles(sourceFile, projectDir) {
54
54
  const pattern = join(dir, `${base}.{test,integration.test}{.ts,.tsx}`);
55
55
  return globSync(pattern, { cwd: projectDir, withFileTypes: false });
56
56
  }
57
+ /**
58
+ * Source files whose tests are NOT colocated.
59
+ *
60
+ * `findTestFiles` maps a changed file to `<same-dir>/<same-basename>.test.ts`.
61
+ * That is right for the common case and wrong for a few "contract" files whose
62
+ * dependent assertions deliberately live elsewhere — a registry that generates
63
+ * docs, a template rendered under a byte budget, and so on. Those tests then
64
+ * stay invisible to `--affected`, so `wp ci-preflight` (whose every stage runs
65
+ * `--affected --branch`) can report "all required checks passed" while they are
66
+ * genuinely failing.
67
+ *
68
+ * Observed 2026-07-24: adding MCP tools broke `scaffold-agents-md`,
69
+ * `package-manifest`, `reference-parity-tool-surface`, and `session-docs`.
70
+ * `wp ci-preflight` was green locally AND the pre-push hook passed; CI caught
71
+ * all of it. Three PRs landed red before the pattern was understood.
72
+ *
73
+ * Patterns are matched against repo-relative POSIX paths. Listed tests are
74
+ * existence-filtered by the caller, so an entry that does not apply to a
75
+ * consumer repo is simply inert.
76
+ */
77
+ const CROSS_CUTTING_TEST_DEPENDENTS = [
78
+ {
79
+ // The compiled MCP tool registry feeds the generated AGENTS.md tool list
80
+ // (and its byte budget), the packed README session-memory contract, and
81
+ // reference-parity classification. None are colocated with the registry.
82
+ when: /^src\/mcp\/tools\/_(names|registry)\.ts$/u,
83
+ tests: [
84
+ "src/cli/commands/init/scaffold-agents-md.test.ts",
85
+ "src/build/package-manifest.test.ts",
86
+ "src/__integration__/reference-parity-tool-surface.integration.test.ts",
87
+ "src/mcp/tools/session-docs.test.ts",
88
+ ],
89
+ },
90
+ {
91
+ // The AGENTS.md template renders into the scaffold byte-budget assertion.
92
+ when: /^catalog\/AGENTS\.md\.tpl$/u,
93
+ tests: ["src/cli/commands/init/scaffold-agents-md.test.ts"],
94
+ },
95
+ {
96
+ // README and the session-memory guide are pinned to the registered
97
+ // session-memory tool set from both directions.
98
+ when: /^(README\.md|docs\/guides\/session-memory\.md)$/u,
99
+ tests: ["src/mcp/tools/session-docs.test.ts", "src/build/package-manifest.test.ts"],
100
+ },
101
+ ];
102
+ /**
103
+ * Every test path the cross-cutting map can yield.
104
+ *
105
+ * Exported so a guard test can assert these still exist on disk. Without that
106
+ * guard a rename would keep the unit tests green — they only compare strings —
107
+ * while the caller's existence filter silently dropped the path, reinstating
108
+ * the very under-selection this map exists to prevent.
109
+ */
110
+ export function crossCuttingTestDependentPaths() {
111
+ const paths = new Set();
112
+ for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
113
+ for (const test of rule.tests)
114
+ paths.add(test);
115
+ }
116
+ return [...paths];
117
+ }
118
+ /** Non-colocated tests a changed file is known to affect. */
119
+ export function findCrossCuttingTestFiles(sourceFile) {
120
+ const normalized = sourceFile.replaceAll("\\", "/");
121
+ const matched = new Set();
122
+ for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
123
+ if (rule.when.test(normalized))
124
+ for (const test of rule.tests)
125
+ matched.add(test);
126
+ }
127
+ return [...matched];
128
+ }
57
129
  export function discoverTestFiles(changedFiles, projectDir) {
58
130
  const testFiles = new Set();
59
131
  for (const file of changedFiles) {
60
132
  for (const testFile of findTestFiles(file, projectDir))
61
133
  testFiles.add(testFile);
134
+ for (const testFile of findCrossCuttingTestFiles(file))
135
+ testFiles.add(testFile);
62
136
  }
63
137
  return [...testFiles];
64
138
  }
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import { parseBlueprint } from "#core/parser";
5
5
  import { setBlueprintFrontmatterFields } from "#lifecycle/engine";
6
- import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
6
+ import { assertCompletedApprovalGate, evaluateApprovalGate, formatApprovalGateRequirement, } from "#lifecycle/audit";
7
7
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
8
8
  import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
9
9
  import { assertAllTasksHaveCanonicalPassingEvidence } from "#verification.js";
@@ -70,6 +70,19 @@ export async function applyLocalBlueprintTransition(input) {
70
70
  throw new Error(`Cannot promote "${slug}" to ${to_state}: frontmatter \`approvals:\` does not satisfy the promotion reviewer mix (${formatApprovalGateRequirement(approvalEvaluation)}). Record approvals, commit the matching review ledger (e.g. reviews.md), and include tracked review transcript/artifact files referenced by each structured review entry — see catalog/agent/rules/pre-implementation.md.`);
71
71
  }
72
72
  }
73
+ // Completion parity for the MCP lifecycle funnel: finalize, promote, and
74
+ // transition all route here, so gate `completed` writes on the same
75
+ // plan-purpose approval CI's `wp audit blueprint-lifecycle` enforces
76
+ // afterward. Reads the raw on-disk markdown (not the trust-gated copy) to
77
+ // mirror the audit's frontmatter parsing exactly.
78
+ if (to_state === "completed") {
79
+ try {
80
+ assertCompletedApprovalGate(overviewPath, markdown);
81
+ }
82
+ catch (error) {
83
+ throw new Error(`Cannot complete "${slug}": ${error instanceof Error ? error.message : String(error)}`);
84
+ }
85
+ }
73
86
  const updated = setBlueprintFrontmatterFields(trustedMarkdown, {
74
87
  status: to_state,
75
88
  last_updated: todayIsoDate(),
@@ -2,14 +2,17 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import matter from "gray-matter";
5
+ import { parseBlueprint } from "#core/parser";
5
6
  import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
7
+ import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
6
8
  import { BlueprintCreationService } from "#service/BlueprintCreationService";
7
9
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
8
- import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
10
+ import { getBlueprintDocumentPaths, isBlueprintSlugSegment, } from "#utils/document-paths.js";
9
11
  import { makeNextAction } from "#next-action.js";
10
12
  import { ensureProjectionReady } from "#projection-ready.js";
11
13
  import { applyPromotionTrustGate } from "#trust/promotion.js";
12
14
  import { validateBlueprintTrust } from "#trust/validator.js";
15
+ import { applyVerification, readTaskVerification } from "#verification.js";
13
16
  import { dbPath, persistBlueprintMarkdown, reIngest } from "#mcp/blueprint/_shared/db";
14
17
  import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
15
18
  import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
@@ -353,6 +356,52 @@ function renderMarkdownTable(headers, rows) {
353
356
  function markdownTableCell(value) {
354
357
  return value.replace(/\s*\n\s*/gu, " ").trim();
355
358
  }
359
+ /**
360
+ * wp_blueprint_put's task schema has no field for the `webpresso-evidence-v1`
361
+ * verification block that wp_blueprint_task_verify writes — evidence is an
362
+ * append-only trust artifact that callers must never freely reconstruct (see
363
+ * catalog/agent/rules/pre-implementation.md). A naive re-render of the whole
364
+ * document therefore silently discards any `**Verification:**` block already
365
+ * recorded for a `done` task.
366
+ *
367
+ * This reads the exact evidence already on disk for every task that has a
368
+ * block, and re-applies it verbatim onto the freshly rendered markdown when
369
+ * that task is still present in the payload with status "done" — the caller
370
+ * never supplies or fabricates evidence content. When the payload cannot
371
+ * represent a protected task (dropped, or moved off "done"), this fails
372
+ * closed with an actionable error instead of destroying the evidence.
373
+ */
374
+ function preserveExistingTaskVerification(existingMarkdown, slug, document, renderedMarkdown) {
375
+ const existingBlueprint = parseBlueprint(existingMarkdown, slug);
376
+ const protectedTasks = existingBlueprint.tasks
377
+ .map((task) => ({ id: task.id, evidence: readTaskVerification(existingMarkdown, task.id) }))
378
+ .filter((entry) => entry.evidence !== null);
379
+ let markdown = renderedMarkdown;
380
+ for (const protectedTask of protectedTasks) {
381
+ const { id, evidence } = protectedTask;
382
+ const submittedTask = document.tasks.find((task) => task.id === id);
383
+ if (!submittedTask || submittedTask.status !== "done") {
384
+ return { error: describeEvidenceLossRefusal(id, submittedTask?.status) };
385
+ }
386
+ const applied = applyVerification(markdown, id, evidence);
387
+ if (!applied.ok) {
388
+ return {
389
+ error: `wp_blueprint_put failed to re-apply preserved verification evidence for task ${id}: ${applied.failures.join("; ")}`,
390
+ };
391
+ }
392
+ markdown = applied.markdown;
393
+ }
394
+ return { markdown };
395
+ }
396
+ function describeEvidenceLossRefusal(taskId, submittedStatus) {
397
+ const disposition = submittedStatus === undefined
398
+ ? `the submitted document does not include task ${taskId}`
399
+ : `the submitted document sets task ${taskId}'s status to "${submittedStatus}"`;
400
+ return (`wp_blueprint_put refused: task ${taskId} already has verification evidence recorded by ` +
401
+ `wp_blueprint_task_verify, but ${disposition} — wp_blueprint_put cannot represent or discard ` +
402
+ `that evidence. Keep task ${taskId} present with status "done" in the payload; put will carry ` +
403
+ `the recorded evidence forward automatically.`);
404
+ }
356
405
  function validatePutTrustDossier(repoRoot, file, markdown) {
357
406
  const validated = validateBlueprintTrust({
358
407
  repoRoot,
@@ -374,6 +423,13 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
374
423
  if (!p.success)
375
424
  return err("wp_blueprint_put validation error", p.error.message);
376
425
  const { project_id, slug, document, request_id, head_at_ingest } = p.data;
426
+ // Validate the slug against the canonical segment pattern before ANY
427
+ // routing or filesystem probe: an unvalidated slug such as "../../outside"
428
+ // would otherwise construct paths outside the blueprint root.
429
+ const slugSegments = slug.split("/");
430
+ if (slugSegments.length === 0 || !slugSegments.every((s) => isBlueprintSlugSegment(s))) {
431
+ return err("wp_blueprint_put validation error", `Invalid blueprint slug "${slug}": every segment must match lowercase [a-z0-9] with single hyphens.`);
432
+ }
377
433
  const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
378
434
  if ("content" in resolvedProject)
379
435
  return resolvedProject;
@@ -408,16 +464,34 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
408
464
  if (found && found.state !== document.status) {
409
465
  return err("wp_blueprint_put refused", `Blueprint "${slug}" currently lives in "${found.state}" and cannot be rewritten as "${document.status}" without a lifecycle transition.`);
410
466
  }
467
+ // Failure atomicity: render and validate against the canonical folder
468
+ // path FIRST, and only canonicalize an existing flat document after every
469
+ // validation has passed. A validation failure must leave the flat file
470
+ // untouched; a persistence failure after conversion leaves the
471
+ // byte-identical document at the canonical folder path (retry-safe).
411
472
  const overviewPath = found
412
- ? found.path
413
- : getBlueprintDocumentPaths(root, document.status, slug).flat;
414
- const markdown = renderBlueprintMarkdownFromDocument(slug, document);
473
+ ? found.shape === "flat"
474
+ ? getBlueprintDocumentPaths(root, found.state, slug).folder
475
+ : found.path
476
+ : getBlueprintDocumentPaths(root, document.status, slug).folder;
477
+ let markdown = renderBlueprintMarkdownFromDocument(slug, document);
478
+ if (found) {
479
+ const existingMarkdown = readFileSync(found.path, "utf8");
480
+ const preserved = preserveExistingTaskVerification(existingMarkdown, slug, document, markdown);
481
+ if ("error" in preserved) {
482
+ return err("wp_blueprint_put refused", preserved.error);
483
+ }
484
+ markdown = preserved.markdown;
485
+ }
415
486
  if (document.trust_dossier) {
416
487
  const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown);
417
488
  if (trustDossierFailure) {
418
489
  return err("wp_blueprint_put validation error", trustDossierFailure);
419
490
  }
420
491
  }
492
+ if (found && found.shape === "flat") {
493
+ await canonicalizeBlueprint(writeCwd, slug);
494
+ }
421
495
  const blueprint = await persistBlueprintMarkdown({
422
496
  projectCwd: writeCwd,
423
497
  slug,
@@ -4,7 +4,7 @@ import matter from "gray-matter";
4
4
  import { z } from "zod";
5
5
  import { logReviewEntry, REVIEWER_IDS } from "#cli/commands/review.js";
6
6
  import { isPolicyApprovalVerdict } from "#review/verdict.js";
7
- import { isBlueprintSlugSegment } from "#utils/document-paths.js";
7
+ import { folderBlueprintRequiredMessage, isBlueprintSlugSegment } from "#utils/document-paths.js";
8
8
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
9
9
  import { writeFileAtomic } from "#shared-utils/write-json-file.js";
10
10
  import { err, finishPayload } from "#mcp/blueprint/_shared/errors";
@@ -98,6 +98,13 @@ export async function handleReviewLog(projectResolver, cwd, raw) {
98
98
  const found = findBlueprintDir(root, slug, ALL_BLUEPRINT_STATES);
99
99
  if (!found)
100
100
  return err("wp_blueprint_review_log failed", `Blueprint "${slug}" not found in any state directory`);
101
+ const shapeFailure = folderBlueprintRequiredMessage({
102
+ documentPath: found.path,
103
+ lifecycleSlug: `${found.state}/${slug}`,
104
+ shape: found.shape,
105
+ });
106
+ if (shapeFailure)
107
+ return err("wp_blueprint_review_log failed", shapeFailure);
101
108
  const blueprintDir = realpathSync(path.dirname(found.path));
102
109
  const artifactAbsolute = path.resolve(blueprintDir, normalizedArtifactPath);
103
110
  if (!artifactAbsolute.startsWith(`${blueprintDir}${path.sep}`)) {
@@ -8,12 +8,53 @@ import { dbPath, reIngest } from "#mcp/blueprint/_shared/db";
8
8
  import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
9
9
  import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
10
10
  import { toStr } from "#mcp/blueprint/_shared/payload";
11
- import { resolveToolProject } from "#mcp/blueprint/_shared/project";
11
+ import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
12
12
  import { resolveSyncAdapter, runPlatformMutationSync } from "#mcp/blueprint/_shared/sync";
13
13
  import { writeFileAtomic } from "#shared-utils/write-json-file.js";
14
14
  import { escapeRegex } from "#utils/string";
15
+ import { withMarkdownWriteLock } from "#db/paths.js";
16
+ import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
17
+ const ALL_STATES = ["draft", "planned", "in-progress", "parked", "archived", "completed"];
18
+ /**
19
+ * Matches the canonical task-heading grammar `parser.ts`'s `extractTasks`
20
+ * uses (`^####\s+(?:\[lane\]\s+)?Task\s+<id>:`), not a looser ad hoc pattern.
21
+ * The id is anchored on a trailing colon so "1.1" cannot match "1.10", and
22
+ * an optional `[lane]` prefix is accepted the same way the canonical parser
23
+ * does.
24
+ */
25
+ function buildTaskHeadingPattern(taskId) {
26
+ return new RegExp(`^####\\s+(?:\\[[^\\]]+\\]\\s+)?Task\\s+${escapeRegex(taskId)}:`, "u");
27
+ }
28
+ function findTaskStatusLine(markdown, taskId) {
29
+ const lines = markdown.split("\n");
30
+ const headingPattern = buildTaskHeadingPattern(taskId);
31
+ let inBlock = false;
32
+ for (let i = 0; i < lines.length; i++) {
33
+ const line = lines[i] ?? "";
34
+ if (headingPattern.test(line))
35
+ inBlock = true;
36
+ else if (inBlock && /^####\s+/u.test(line))
37
+ break;
38
+ else if (inBlock) {
39
+ // Preserve any inline metadata sharing the status line (the canonical
40
+ // format allows `**Status:** done | **Depends:** Task 1.1`) instead of
41
+ // overwriting the whole line and discarding it.
42
+ const match = line.match(/^(\*\*Status:\*\*\s*)(\S+)(.*)$/u);
43
+ if (match) {
44
+ return {
45
+ lineIndex: i,
46
+ prefix: match[1] ?? "",
47
+ status: match[2] ?? "",
48
+ suffix: match[3] ?? "",
49
+ };
50
+ }
51
+ }
52
+ }
53
+ return null;
54
+ }
15
55
  const advanceSchema = z.object({
16
56
  project_id: z.string(),
57
+ slug: z.string(),
17
58
  task_id: z.string(),
18
59
  to: z.enum(["todo", "in-progress", "blocked", "done", "dropped"]),
19
60
  request_id: z.string().min(1).optional(),
@@ -23,7 +64,7 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
23
64
  const p = advanceSchema.safeParse(raw);
24
65
  if (!p.success)
25
66
  return err("wp_blueprint_task_advance validation error", p.error.message);
26
- const { project_id, task_id, to, request_id, head_at_ingest } = p.data;
67
+ const { project_id, slug, task_id, to, request_id, head_at_ingest } = p.data;
27
68
  const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
28
69
  if ("content" in resolvedProject)
29
70
  return resolvedProject;
@@ -32,7 +73,7 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
32
73
  const freshnessFailure = validateMutationFreshnessToken(projectCwd, head_at_ingest, "wp_blueprint_task_advance", "wp_blueprint_list");
33
74
  if (freshnessFailure)
34
75
  return freshnessFailure;
35
- const payloadHash = hashMutationPayload({ task_id, to });
76
+ const payloadHash = hashMutationPayload({ slug, task_id, to });
36
77
  const replay = request_id !== undefined
37
78
  ? readMutationReplay(projectCwd, "wp_blueprint_task_advance", request_id, payloadHash)
38
79
  : null;
@@ -54,70 +95,88 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
54
95
  return err("wp_blueprint_task_advance failed", "Blueprint DB not found");
55
96
  try {
56
97
  const conn = openDb(target);
57
- let oldStatus = null;
58
- let filePath = null;
59
- let blueprintSlug = null;
60
98
  try {
61
99
  const row = conn.db
62
- .prepare("SELECT status, blueprint_slug FROM tasks WHERE task_id = ? LIMIT 1")
63
- .get(task_id);
100
+ .prepare("SELECT status FROM tasks WHERE task_id = ? AND blueprint_slug = ? LIMIT 1")
101
+ .get(task_id, slug);
64
102
  if (!row)
65
- return err("wp_blueprint_task_advance failed", `Task "${task_id}" not found in DB`);
66
- oldStatus = row.status;
67
- blueprintSlug = row.blueprint_slug;
68
- const bp = conn.db
69
- .prepare("SELECT file_path FROM blueprints WHERE slug = ?")
70
- .get(row.blueprint_slug);
71
- if (bp?.file_path)
72
- filePath = bp.file_path;
103
+ return err("wp_blueprint_task_advance failed", `Task "${task_id}" not found in blueprint "${slug}"`);
73
104
  }
74
105
  finally {
75
106
  conn.close();
76
107
  }
108
+ // Derive the authoritative fromStatus from a live read of the blueprint's
109
+ // on-disk markdown, not the SQLite projection above (which is only
110
+ // re-ingested asynchronously and can be stale relative to the file --
111
+ // e.g. after a blueprint move/promotion changed its lifecycle directory).
112
+ // Fail closed here rather than proceeding with a guessed value, or later
113
+ // silently reporting success when the local write never happens.
114
+ const root = resolveBlueprintRoot(projectCwd);
115
+ const foundBeforePush = findBlueprintDir(root, slug, ALL_STATES);
116
+ if (!foundBeforePush || !existsSync(foundBeforePush.path)) {
117
+ return err("wp_blueprint_task_advance failed", `Blueprint "${slug}" overview file not found`);
118
+ }
119
+ const preStatus = findTaskStatusLine(readFileSync(foundBeforePush.path, "utf8"), task_id);
120
+ if (!preStatus) {
121
+ return err("wp_blueprint_task_advance failed", `Task "${task_id}" has no **Status:** line in blueprint "${slug}"`);
122
+ }
123
+ const oldStatus = preStatus.status;
77
124
  // Platform-first path: push event + pull fresh replica before local update.
78
125
  // Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
79
126
  const adapter = await resolveSyncAdapter(projectCwd);
80
- if (blueprintSlug !== null && oldStatus !== null) {
81
- try {
82
- await runPlatformMutationSync(adapter, {
83
- label: "wp_blueprint_task_advance",
84
- event: {
85
- eventId: randomUUID(),
86
- repoId: process.env["WP_BLUEPRINT_PLATFORM_REPO_ID"] ?? "local",
87
- occurredAt: new Date().toISOString(),
127
+ try {
128
+ await runPlatformMutationSync(adapter, {
129
+ label: "wp_blueprint_task_advance",
130
+ event: {
131
+ eventId: randomUUID(),
132
+ repoId: process.env["WP_BLUEPRINT_PLATFORM_REPO_ID"] ?? "local",
133
+ occurredAt: new Date().toISOString(),
134
+ type: "task.status_changed",
135
+ payload: {
88
136
  type: "task.status_changed",
89
- payload: {
90
- type: "task.status_changed",
91
- blueprintSlug,
92
- taskId: task_id,
93
- fromStatus: oldStatus,
94
- toStatus: to,
95
- },
137
+ blueprintSlug: slug,
138
+ taskId: task_id,
139
+ fromStatus: oldStatus,
140
+ toStatus: to,
96
141
  },
97
- ensureFreshSlug: blueprintSlug,
98
- });
99
- }
100
- catch (e) {
101
- return err("wp_blueprint_task_advance failed", toStr(e));
102
- }
142
+ },
143
+ ensureFreshSlug: slug,
144
+ });
103
145
  }
104
- // Always update local markdown + SQLite.
105
- // Platform-first: these become derived artifacts; disabled: these are canonical.
106
- if (filePath && existsSync(filePath)) {
107
- const lines = readFileSync(filePath, "utf8").split("\n");
108
- let inBlock = false;
109
- for (let i = 0; i < lines.length; i++) {
110
- const line = lines[i] ?? "";
111
- if (line.match(new RegExp(`#### Task\\s+${escapeRegex(task_id)}`)))
112
- inBlock = true;
113
- else if (inBlock && line.startsWith("#### "))
114
- break;
115
- else if (inBlock && line.startsWith("**Status:**")) {
116
- lines[i] = `**Status:** ${to}`;
117
- break;
118
- }
119
- }
120
- writeFileAtomic(filePath, lines.join("\n"), "utf8");
146
+ catch (e) {
147
+ return err("wp_blueprint_task_advance failed", toStr(e));
148
+ }
149
+ // Always update local markdown + SQLite. Re-resolve the path live INSIDE
150
+ // the repo-scoped write lock: a concurrent promotion can rename the
151
+ // blueprint's directory between the pre-push read above and this write.
152
+ // A missing blueprint or status line here is a genuine race, not
153
+ // something to paper over as success -- `wrote` makes that failure
154
+ // observable to the caller instead of silently no-op'ing.
155
+ let wrote = false;
156
+ await withMarkdownWriteLock(projectCwd, () => {
157
+ const liveFound = findBlueprintDir(root, slug, ALL_STATES);
158
+ const livePath = liveFound?.path;
159
+ if (!livePath || !existsSync(livePath))
160
+ return;
161
+ const markdown = readFileSync(livePath, "utf8");
162
+ const live = findTaskStatusLine(markdown, task_id);
163
+ if (!live)
164
+ return;
165
+ const lines = markdown.split("\n");
166
+ lines[live.lineIndex] = `${live.prefix}${to}${live.suffix}`;
167
+ writeFileAtomic(livePath, lines.join("\n"), "utf8");
168
+ wrote = true;
169
+ });
170
+ if (!wrote) {
171
+ // The platform mutation above already succeeded by this point (or was
172
+ // a no-op if platform sync is disabled) -- this failure therefore
173
+ // means platform and local state have genuinely diverged, not just
174
+ // that the local write was skipped harmlessly. There is no automatic
175
+ // reconciliation path today (that requires the platform-first
176
+ // mutation reconciliation work tracked separately, not built here);
177
+ // surface the divergence explicitly rather than a generic error so an
178
+ // operator knows this needs manual attention, not just a retry.
179
+ return err("wp_blueprint_task_advance failed", `Task "${task_id}" in blueprint "${slug}" could not be updated locally after the platform mutation already succeeded (blueprint or status line changed concurrently). Platform and local state have diverged and require manual reconciliation -- retrying this call alone will not fix it if the blueprint moved again.`);
121
180
  }
122
181
  try {
123
182
  await reIngest(projectCwd);