@webpresso/agent-kit 3.1.30 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +183 -20
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/blueprint-trust.js +14 -10
  17. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  18. package/dist/esm/audit/changeset-evidence.js +30 -0
  19. package/dist/esm/audit/changeset-required.d.ts +10 -0
  20. package/dist/esm/audit/changeset-required.js +73 -0
  21. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  22. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  24. package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
  25. package/dist/esm/audit/ci-test-perf.js +30 -0
  26. package/dist/esm/audit/command-surface.js +4 -2
  27. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  28. package/dist/esm/audit/host-substitution-risk.js +256 -0
  29. package/dist/esm/audit/registry.d.ts +4 -4
  30. package/dist/esm/audit/registry.js +13 -1
  31. package/dist/esm/audit/toolchain-isolation.js +1 -2
  32. package/dist/esm/audit/typescript-version.d.ts +2 -0
  33. package/dist/esm/audit/typescript-version.js +123 -0
  34. package/dist/esm/blueprint/core/parser.js +101 -2
  35. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  36. package/dist/esm/blueprint/core/schema.js +26 -0
  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 +87 -20
  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/local.d.ts +2 -1
  46. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  47. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  48. package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
  49. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  50. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  51. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  52. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  53. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  54. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  55. package/dist/esm/blueprint/sync/client.js +1 -1
  56. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  57. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  58. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  59. package/dist/esm/blueprint/trust/dossier.js +40 -6
  60. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  61. package/dist/esm/blueprint/trust/gates.js +20 -0
  62. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  63. package/dist/esm/blueprint/trust/promotion.js +74 -112
  64. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  65. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  66. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  67. package/dist/esm/blueprint/trust/validator.js +7 -1
  68. package/dist/esm/blueprint/utils/archive.js +27 -5
  69. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  70. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  71. package/dist/esm/build/cli-mcp-parity.js +15 -0
  72. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  73. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  74. package/dist/esm/cli/cli.d.ts +1 -1
  75. package/dist/esm/cli/cli.js +43 -0
  76. package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
  77. package/dist/esm/cli/commands/agent-launch.js +93 -4
  78. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  79. package/dist/esm/cli/commands/audit-core.js +8 -1
  80. package/dist/esm/cli/commands/audit.js +21 -5
  81. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  82. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  83. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  84. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  85. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  86. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  87. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  88. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  89. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  90. package/dist/esm/cli/commands/ci-preflight.js +30 -6
  91. package/dist/esm/cli/commands/compile.js +5 -31
  92. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  93. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  94. package/dist/esm/cli/commands/doctor.js +5 -0
  95. package/dist/esm/cli/commands/hook.d.ts +5 -0
  96. package/dist/esm/cli/commands/hook.js +15 -9
  97. package/dist/esm/cli/commands/init/config.js +2 -2
  98. package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
  99. package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
  100. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  101. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  102. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  103. package/dist/esm/cli/commands/init/convergence.js +57 -0
  104. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  105. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  106. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  107. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  108. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  109. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  110. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  111. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  112. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  113. package/dist/esm/cli/commands/init/index.d.ts +3 -0
  114. package/dist/esm/cli/commands/init/index.js +326 -173
  115. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  116. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  117. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  118. package/dist/esm/cli/commands/init/merge.js +30 -1
  119. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  120. package/dist/esm/cli/commands/init/package-root.js +10 -2
  121. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  122. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  123. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  124. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  125. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
  126. package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
  127. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  128. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  129. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  130. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
  131. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  132. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  133. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
  134. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
  135. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  136. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  137. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  138. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  139. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  140. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  141. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  142. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  143. package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
  144. package/dist/esm/cli/commands/opencode-rotate.js +147 -0
  145. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  146. package/dist/esm/cli/commands/public-readiness.js +42 -0
  147. package/dist/esm/cli/commands/quality-runner.js +3 -3
  148. package/dist/esm/cli/commands/review.d.ts +35 -3
  149. package/dist/esm/cli/commands/review.js +751 -209
  150. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  151. package/dist/esm/cli/commands/run-wait.js +122 -0
  152. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  153. package/dist/esm/cli/commands/rust-check.js +110 -0
  154. package/dist/esm/cli/commands/setup-help.js +1 -1
  155. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  156. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  157. package/dist/esm/cli/commands/typecheck.js +30 -1
  158. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  159. package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
  160. package/dist/esm/cli/commands/worktree/router.js +10 -0
  161. package/dist/esm/cli/direct-provider-launch.js +9 -5
  162. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  163. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  164. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  165. package/dist/esm/daemon/domains/git-actions.js +136 -0
  166. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  167. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  168. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  169. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  170. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  171. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  172. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  173. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  174. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  175. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  176. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  177. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  178. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  179. package/dist/esm/daemon/handlers/product.js +10 -119
  180. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  181. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  182. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  183. package/dist/esm/daemon/handlers/shared.js +16 -0
  184. package/dist/esm/daemon/methods.d.ts +8 -1
  185. package/dist/esm/daemon/methods.js +7 -0
  186. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  187. package/dist/esm/daemon/protocol/contract.js +26 -0
  188. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  189. package/dist/esm/daemon/protocol/index.js +1 -1
  190. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  191. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  192. package/dist/esm/hooks/conformance/batch.js +5 -0
  193. package/dist/esm/hooks/doctor.d.ts +5 -3
  194. package/dist/esm/hooks/doctor.js +67 -16
  195. package/dist/esm/hooks/permission-request/index.js +9 -3
  196. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  197. package/dist/esm/hooks/permission-request/policy.js +13 -0
  198. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  199. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  200. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  201. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  202. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  203. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  204. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  205. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  206. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  207. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  208. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  209. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  210. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  211. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  212. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  213. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  214. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  215. package/dist/esm/hooks/shared/types.d.ts +8 -5
  216. package/dist/esm/hooks/shared/types.js +50 -9
  217. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  218. package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
  219. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  220. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  221. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
  222. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  223. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  224. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  225. package/dist/esm/mcp/blueprint/registration.js +3 -2
  226. package/dist/esm/mcp/server.d.ts +1 -0
  227. package/dist/esm/mcp/server.js +27 -0
  228. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  229. package/dist/esm/mcp/tools/_names.js +5 -0
  230. package/dist/esm/mcp/tools/_registry.js +10 -0
  231. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  232. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  233. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  234. package/dist/esm/mcp/tools/audit.js +42 -0
  235. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  236. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  237. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  238. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  239. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  240. package/dist/esm/mcp/tools/review-run.js +216 -0
  241. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  242. package/dist/esm/mcp/tools/run-wait.js +593 -0
  243. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  244. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  245. package/dist/esm/mcp/tools/session-id.js +45 -0
  246. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  247. package/dist/esm/mcp/tools/session-info.js +49 -0
  248. package/dist/esm/output-transforms/audit.d.ts +23 -0
  249. package/dist/esm/output-transforms/audit.js +47 -0
  250. package/dist/esm/output-transforms/index.js +2 -0
  251. package/dist/esm/package.json +2 -0
  252. package/dist/esm/platform/client.d.ts +7 -1
  253. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  254. package/dist/esm/pr-description/contract.js +24 -4
  255. package/dist/esm/review/authority.js +27 -14
  256. package/dist/esm/review/availability.d.ts +33 -0
  257. package/dist/esm/review/availability.js +122 -20
  258. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  259. package/dist/esm/review/delivery-verifier.js +115 -0
  260. package/dist/esm/review/events.d.ts +3 -2
  261. package/dist/esm/review/events.js +23 -3
  262. package/dist/esm/review/execution/adapters.d.ts +11 -1
  263. package/dist/esm/review/execution/adapters.js +36 -10
  264. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  265. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  266. package/dist/esm/review/execution/output-policy.js +12 -0
  267. package/dist/esm/review/execution/review-checkout.js +121 -21
  268. package/dist/esm/review/execution/supervisor.js +36 -4
  269. package/dist/esm/review/execution/types.d.ts +9 -0
  270. package/dist/esm/review/opencode-account.d.ts +24 -0
  271. package/dist/esm/review/opencode-account.js +68 -0
  272. package/dist/esm/review/opencode-auth.d.ts +33 -0
  273. package/dist/esm/review/opencode-auth.js +79 -0
  274. package/dist/esm/review/subject.d.ts +25 -0
  275. package/dist/esm/review/subject.js +116 -26
  276. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  277. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  278. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  279. package/dist/esm/rust/affected-manifests.js +69 -0
  280. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  281. package/dist/esm/status/snapshot.js +32 -25
  282. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  283. package/dist/esm/test/duration-sequencer.js +54 -0
  284. package/dist/esm/test/shard-durations.json +922 -0
  285. package/dist/esm/test/worker-budget.d.ts +6 -0
  286. package/dist/esm/test/worker-budget.js +59 -1
  287. package/dist/esm/test-helpers/global-setup.d.ts +1 -2
  288. package/dist/esm/test-helpers/global-setup.js +79 -32
  289. package/dist/esm/typecheck/planner.d.ts +9 -1
  290. package/dist/esm/typecheck/planner.js +31 -0
  291. package/dist/esm/ultragoal/service.d.ts +7 -0
  292. package/dist/esm/ultragoal/service.js +42 -2
  293. package/dist/esm/utils/write-json-file.js +3 -1
  294. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  295. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  296. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  297. package/dist/esm/worktrees/orphan-scan.js +211 -0
  298. package/dist/esm/worktrees/registry.d.ts +8 -0
  299. package/dist/esm/worktrees/registry.js +18 -3
  300. package/package.json +18 -13
@@ -54,11 +54,103 @@ 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
+ // blueprint-server.ts owns the MCP architecture contract (tool naming,
103
+ // handler wiring, transport shape). Its architecture test is not colocated
104
+ // under the same basename, so --affected would otherwise skip it.
105
+ when: /^src\/mcp\/blueprint-server\./u,
106
+ tests: ["src/mcp/blueprint-server.test-architecture.test.ts"],
107
+ },
108
+ {
109
+ // Package surface / publishability contracts live in the audit package,
110
+ // not next to package.json or the manifest builder.
111
+ when: /^(package\.json|src\/build\/package-manifest\.ts)$/u,
112
+ tests: ["src/audit/package-surface.test.ts"],
113
+ },
114
+ {
115
+ // Marketplace and plugin.json validation is a build-side contract test.
116
+ when: /^\.claude-plugin\/(marketplace|plugin)\.json$/u,
117
+ tests: ["src/build/validate-marketplace.test.ts"],
118
+ },
119
+ ];
120
+ /**
121
+ * Every test path the cross-cutting map can yield.
122
+ *
123
+ * Exported so a guard test can assert these still exist on disk. Without that
124
+ * guard a rename would keep the unit tests green — they only compare strings —
125
+ * while the caller's existence filter silently dropped the path, reinstating
126
+ * the very under-selection this map exists to prevent.
127
+ */
128
+ export function crossCuttingTestDependentPaths() {
129
+ const paths = new Set();
130
+ for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
131
+ for (const test of rule.tests)
132
+ paths.add(test);
133
+ }
134
+ return [...paths];
135
+ }
136
+ /** Non-colocated tests a changed file is known to affect. */
137
+ export function findCrossCuttingTestFiles(sourceFile) {
138
+ const normalized = sourceFile.replaceAll("\\", "/");
139
+ const matched = new Set();
140
+ for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
141
+ if (rule.when.test(normalized))
142
+ for (const test of rule.tests)
143
+ matched.add(test);
144
+ }
145
+ return [...matched];
146
+ }
57
147
  export function discoverTestFiles(changedFiles, projectDir) {
58
148
  const testFiles = new Set();
59
149
  for (const file of changedFiles) {
60
150
  for (const testFile of findTestFiles(file, projectDir))
61
151
  testFiles.add(testFile);
152
+ for (const testFile of findCrossCuttingTestFiles(file))
153
+ testFiles.add(testFile);
62
154
  }
63
155
  return [...testFiles];
64
156
  }
@@ -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(),
@@ -194,6 +194,11 @@ export declare const putInputJsonSchema: {
194
194
  last_result: {
195
195
  type: string;
196
196
  };
197
+ defer: {
198
+ type: string;
199
+ const: string;
200
+ description: string;
201
+ };
197
202
  };
198
203
  required: string[];
199
204
  additionalProperties: boolean;
@@ -2,14 +2,18 @@ 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";
6
+ import { taskIdSchema } from "#core/schema";
5
7
  import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
8
+ import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
6
9
  import { BlueprintCreationService } from "#service/BlueprintCreationService";
7
10
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
8
- import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
11
+ import { getBlueprintDocumentPaths, isBlueprintSlugSegment, } from "#utils/document-paths.js";
9
12
  import { makeNextAction } from "#next-action.js";
10
13
  import { ensureProjectionReady } from "#projection-ready.js";
11
14
  import { applyPromotionTrustGate } from "#trust/promotion.js";
12
15
  import { validateBlueprintTrust } from "#trust/validator.js";
16
+ import { applyVerification, readTaskVerification } from "#verification.js";
13
17
  import { dbPath, persistBlueprintMarkdown, reIngest } from "#mcp/blueprint/_shared/db";
14
18
  import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
15
19
  import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
@@ -37,7 +41,7 @@ const createSchema = MutationTarget.extend({
37
41
  head_at_ingest: z.string().nullable().optional(),
38
42
  });
39
43
  const putTaskSchema = z.object({
40
- id: z.string().min(1),
44
+ id: taskIdSchema,
41
45
  title: z.string().min(1),
42
46
  status: z.enum(["todo", "in-progress", "blocked", "done", "dropped"]).default("todo"),
43
47
  wave: z.string().optional(),
@@ -69,6 +73,12 @@ const putPromotionGateSchema = z.object({
69
73
  command: z.string().min(1),
70
74
  expected_outcome: z.string().min(1),
71
75
  last_result: z.string().min(1),
76
+ // Explicit per-gate pre-implementation deferral. When set, `wp blueprint
77
+ // promote` skips executing/existence-checking this gate's command entirely
78
+ // and records a `deferred: ...` Last result instead. See
79
+ // src/blueprint/trust/promotion.ts and catalog/agent/rules
80
+ // /pre-implementation.md's Promotion gate section.
81
+ defer: z.literal("pre-implementation").optional(),
72
82
  });
73
83
  const putTrustDossierSchema = z.object({
74
84
  readiness: putTrustReadinessSchema,
@@ -177,6 +187,11 @@ const putTrustDossierJsonSchema = {
177
187
  command: { type: "string", description: "wp or ./bin/wp facade command" },
178
188
  expected_outcome: { type: "string" },
179
189
  last_result: { type: "string" },
190
+ defer: {
191
+ type: "string",
192
+ const: "pre-implementation",
193
+ description: "Explicit opt-in: skip executing/existence-checking this gate at promote time",
194
+ },
180
195
  },
181
196
  required: ["gate", "command", "expected_outcome", "last_result"],
182
197
  additionalProperties: false,
@@ -320,12 +335,7 @@ function renderTrustDossierMarkdown(dossier) {
320
335
  "",
321
336
  "### Promotion Gates",
322
337
  "",
323
- ...renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result"], dossier.promotion_gates.map((gate) => [
324
- gate.gate,
325
- gate.command,
326
- gate.expected_outcome,
327
- gate.last_result,
328
- ])),
338
+ ...renderPromotionGatesTable(dossier.promotion_gates),
329
339
  "",
330
340
  "### Residual Unknowns",
331
341
  "",
@@ -333,6 +343,23 @@ function renderTrustDossierMarkdown(dossier) {
333
343
  "",
334
344
  ];
335
345
  }
346
+ // Only emits the (opt-in) Defer column when at least one gate uses it, so
347
+ // the common no-defer case keeps rendering the legacy 4-column table exactly
348
+ // as before -- matching src/blueprint/trust/dossier.ts's parser, which
349
+ // accepts either width.
350
+ function renderPromotionGatesTable(gates) {
351
+ const hasDefer = gates.some((gate) => gate.defer !== undefined);
352
+ if (!hasDefer) {
353
+ return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result"], gates.map((gate) => [gate.gate, gate.command, gate.expected_outcome, gate.last_result]));
354
+ }
355
+ return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result", "Defer"], gates.map((gate) => [
356
+ gate.gate,
357
+ gate.command,
358
+ gate.expected_outcome,
359
+ gate.last_result,
360
+ gate.defer ?? "",
361
+ ]));
362
+ }
336
363
  function normalizeResidualUnknowns(value) {
337
364
  if (value === undefined)
338
365
  return "None.";
@@ -353,6 +380,52 @@ function renderMarkdownTable(headers, rows) {
353
380
  function markdownTableCell(value) {
354
381
  return value.replace(/\s*\n\s*/gu, " ").trim();
355
382
  }
383
+ /**
384
+ * wp_blueprint_put's task schema has no field for the `webpresso-evidence-v1`
385
+ * verification block that wp_blueprint_task_verify writes — evidence is an
386
+ * append-only trust artifact that callers must never freely reconstruct (see
387
+ * catalog/agent/rules/pre-implementation.md). A naive re-render of the whole
388
+ * document therefore silently discards any `**Verification:**` block already
389
+ * recorded for a `done` task.
390
+ *
391
+ * This reads the exact evidence already on disk for every task that has a
392
+ * block, and re-applies it verbatim onto the freshly rendered markdown when
393
+ * that task is still present in the payload with status "done" — the caller
394
+ * never supplies or fabricates evidence content. When the payload cannot
395
+ * represent a protected task (dropped, or moved off "done"), this fails
396
+ * closed with an actionable error instead of destroying the evidence.
397
+ */
398
+ function preserveExistingTaskVerification(existingMarkdown, slug, document, renderedMarkdown) {
399
+ const existingBlueprint = parseBlueprint(existingMarkdown, slug);
400
+ const protectedTasks = existingBlueprint.tasks
401
+ .map((task) => ({ id: task.id, evidence: readTaskVerification(existingMarkdown, task.id) }))
402
+ .filter((entry) => entry.evidence !== null);
403
+ let markdown = renderedMarkdown;
404
+ for (const protectedTask of protectedTasks) {
405
+ const { id, evidence } = protectedTask;
406
+ const submittedTask = document.tasks.find((task) => task.id === id);
407
+ if (!submittedTask || submittedTask.status !== "done") {
408
+ return { error: describeEvidenceLossRefusal(id, submittedTask?.status) };
409
+ }
410
+ const applied = applyVerification(markdown, id, evidence);
411
+ if (!applied.ok) {
412
+ return {
413
+ error: `wp_blueprint_put failed to re-apply preserved verification evidence for task ${id}: ${applied.failures.join("; ")}`,
414
+ };
415
+ }
416
+ markdown = applied.markdown;
417
+ }
418
+ return { markdown };
419
+ }
420
+ function describeEvidenceLossRefusal(taskId, submittedStatus) {
421
+ const disposition = submittedStatus === undefined
422
+ ? `the submitted document does not include task ${taskId}`
423
+ : `the submitted document sets task ${taskId}'s status to "${submittedStatus}"`;
424
+ return (`wp_blueprint_put refused: task ${taskId} already has verification evidence recorded by ` +
425
+ `wp_blueprint_task_verify, but ${disposition} — wp_blueprint_put cannot represent or discard ` +
426
+ `that evidence. Keep task ${taskId} present with status "done" in the payload; put will carry ` +
427
+ `the recorded evidence forward automatically.`);
428
+ }
356
429
  function validatePutTrustDossier(repoRoot, file, markdown) {
357
430
  const validated = validateBlueprintTrust({
358
431
  repoRoot,
@@ -374,6 +447,13 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
374
447
  if (!p.success)
375
448
  return err("wp_blueprint_put validation error", p.error.message);
376
449
  const { project_id, slug, document, request_id, head_at_ingest } = p.data;
450
+ // Validate the slug against the canonical segment pattern before ANY
451
+ // routing or filesystem probe: an unvalidated slug such as "../../outside"
452
+ // would otherwise construct paths outside the blueprint root.
453
+ const slugSegments = slug.split("/");
454
+ if (slugSegments.length === 0 || !slugSegments.every((s) => isBlueprintSlugSegment(s))) {
455
+ return err("wp_blueprint_put validation error", `Invalid blueprint slug "${slug}": every segment must match lowercase [a-z0-9] with single hyphens.`);
456
+ }
377
457
  const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
378
458
  if ("content" in resolvedProject)
379
459
  return resolvedProject;
@@ -408,16 +488,34 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
408
488
  if (found && found.state !== document.status) {
409
489
  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
490
  }
491
+ // Failure atomicity: render and validate against the canonical folder
492
+ // path FIRST, and only canonicalize an existing flat document after every
493
+ // validation has passed. A validation failure must leave the flat file
494
+ // untouched; a persistence failure after conversion leaves the
495
+ // byte-identical document at the canonical folder path (retry-safe).
411
496
  const overviewPath = found
412
- ? found.path
413
- : getBlueprintDocumentPaths(root, document.status, slug).flat;
414
- const markdown = renderBlueprintMarkdownFromDocument(slug, document);
497
+ ? found.shape === "flat"
498
+ ? getBlueprintDocumentPaths(root, found.state, slug).folder
499
+ : found.path
500
+ : getBlueprintDocumentPaths(root, document.status, slug).folder;
501
+ let markdown = renderBlueprintMarkdownFromDocument(slug, document);
502
+ if (found) {
503
+ const existingMarkdown = readFileSync(found.path, "utf8");
504
+ const preserved = preserveExistingTaskVerification(existingMarkdown, slug, document, markdown);
505
+ if ("error" in preserved) {
506
+ return err("wp_blueprint_put refused", preserved.error);
507
+ }
508
+ markdown = preserved.markdown;
509
+ }
415
510
  if (document.trust_dossier) {
416
511
  const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown);
417
512
  if (trustDossierFailure) {
418
513
  return err("wp_blueprint_put validation error", trustDossierFailure);
419
514
  }
420
515
  }
516
+ if (found && found.shape === "flat") {
517
+ await canonicalizeBlueprint(writeCwd, slug);
518
+ }
421
519
  const blueprint = await persistBlueprintMarkdown({
422
520
  projectCwd: writeCwd,
423
521
  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);