@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
@@ -42,12 +42,38 @@ function sentenceCase(value) {
42
42
  }
43
43
  return value.slice(0, 1).toUpperCase() + value.slice(1);
44
44
  }
45
+ // A slug becomes a branch name, worktree directory, and repeated CLI argument,
46
+ // so an unbounded goal string must not produce an unbounded slug. Cap it at a
47
+ // word AND a character ceiling (whichever binds first), truncating on a word
48
+ // boundary. The full goal survives as the human-readable title; only the slug
49
+ // is bounded. Collision suffixing runs AFTER this cap, so `-2` is never counted
50
+ // against the ceiling.
51
+ const MAX_SLUG_WORDS = 6;
52
+ const MAX_SLUG_CHARS = 50;
53
+ // Bound on atomic slug-folder reservation retries under concurrent creation.
54
+ const MAX_SLUG_RESERVATION_ATTEMPTS = 50;
55
+ function capSlug(slug) {
56
+ const [firstWord, ...rest] = slug.split("-").filter(Boolean).slice(0, MAX_SLUG_WORDS);
57
+ if (firstWord === undefined)
58
+ return slug;
59
+ let capped = firstWord;
60
+ for (const word of rest) {
61
+ const candidate = `${capped}-${word}`;
62
+ if (candidate.length > MAX_SLUG_CHARS)
63
+ break;
64
+ capped = candidate;
65
+ }
66
+ // A single first word longer than the ceiling is hard-trimmed; drop any
67
+ // trailing hyphen the trim could expose.
68
+ return capped.slice(0, MAX_SLUG_CHARS).replace(/-+$/u, "");
69
+ }
45
70
  function deriveSlug(goal) {
46
- return goal
71
+ const slug = goal
47
72
  .toLowerCase()
48
73
  .replace(/[^a-z0-9]+/g, "-")
49
74
  .replace(/^-+|-+$/g, "")
50
75
  .replace(/-{2,}/g, "-");
76
+ return capSlug(slug);
51
77
  }
52
78
  function assertGoalProducesUsableSlug(goal, slug) {
53
79
  if (slug.length === 0) {
@@ -223,11 +249,16 @@ export class BlueprintCreationService {
223
249
  async create(input) {
224
250
  // H-009: refuse primary-like roots so drafts never leak onto main checkouts.
225
251
  assertDraftAuthoringAllowed(this.projectRoot);
226
- const draft = await this.compileDraft(input);
227
252
  const draftRoot = path.join(this.blueprintsRoot, "draft");
228
- const finalPath = draft.outputPath;
229
253
  await mkdir(draftRoot, { recursive: true });
230
- await mkdir(path.dirname(finalPath), { recursive: true });
254
+ // Atomically RESERVE the slug folder. Two concurrent creations (CLI *and*
255
+ // MCP) can each resolve the same collision-safe slug before either writes;
256
+ // a plain recursive mkdir + rename would then let the later writer silently
257
+ // overwrite the earlier draft. `mkdir(..., { recursive: false })` is atomic:
258
+ // exactly one racer creates the folder, the loser gets EEXIST and re-resolves
259
+ // to the next `-N` suffix (compileDraft re-scans and sees the reservation).
260
+ const draft = await this.reserveDraftSlugFolder(input);
261
+ const finalPath = draft.outputPath;
231
262
  const tempDir = await mkdtemp(path.join(draftRoot, `${draft.slug}.tmp-`));
232
263
  const tempPath = path.join(tempDir, `${draft.slug}.md`);
233
264
  try {
@@ -252,6 +283,24 @@ export class BlueprintCreationService {
252
283
  throw error;
253
284
  }
254
285
  }
286
+ // Compile a draft and atomically claim its slug folder, retrying with the next
287
+ // collision-safe slug when a concurrent creator wins the folder first.
288
+ async reserveDraftSlugFolder(input) {
289
+ for (let attempt = 0; attempt < MAX_SLUG_RESERVATION_ATTEMPTS; attempt += 1) {
290
+ const candidate = await this.compileDraft(input);
291
+ try {
292
+ await mkdir(path.dirname(candidate.outputPath), { recursive: false });
293
+ return candidate;
294
+ }
295
+ catch (error) {
296
+ // Folder already reserved by a concurrent creator — re-resolve the slug.
297
+ if (error instanceof Error && error.code === "EEXIST")
298
+ continue;
299
+ throw error;
300
+ }
301
+ }
302
+ throw new Error(`Could not reserve a unique blueprint slug after ${MAX_SLUG_RESERVATION_ATTEMPTS} attempts.`);
303
+ }
255
304
  resolveCollisionSafeSlug(baseSlug) {
256
305
  const blueprintsRoot = this.blueprintsRoot;
257
306
  const existing = new Set(scanBlueprintDirectory({
@@ -1,15 +1,29 @@
1
1
  import matter from "gray-matter";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { getBlueprintDocumentCandidates } from "#utils/document-paths.js";
4
5
  import { resolveTechDebtRoot } from "#utils/tech-debt-root";
6
+ async function resolveBlueprintDocumentPath(baseDir, bpSlug) {
7
+ const candidates = getBlueprintDocumentCandidates(baseDir, bpSlug);
8
+ for (const candidate of candidates) {
9
+ try {
10
+ await fs.access(candidate);
11
+ return candidate;
12
+ }
13
+ catch {
14
+ // Keep trying the other canonical shape.
15
+ }
16
+ }
17
+ return candidates.at(-1) ?? path.join(baseDir, bpSlug, "_overview.md");
18
+ }
5
19
  export async function linkBlueprintToTechDebt(baseDir, projectPath, bpSlug, tdSlug) {
6
- const blueprintPath = path.join(baseDir, bpSlug, "_overview.md");
20
+ const blueprintPath = await resolveBlueprintDocumentPath(baseDir, bpSlug);
7
21
  await updateLinkedSlugs(blueprintPath, "linked_tech_debt_slugs", (linked) => [...linked, tdSlug]);
8
22
  const tdPath = path.join(resolveTechDebtRoot(projectPath), tdSlug, "README.md");
9
23
  await updateLinkedSlugs(tdPath, "linked_blueprints", (linked) => [...linked, bpSlug]);
10
24
  }
11
25
  export async function unlinkBlueprintFromTechDebt(baseDir, projectPath, bpSlug, tdSlug) {
12
- const blueprintPath = path.join(baseDir, bpSlug, "_overview.md");
26
+ const blueprintPath = await resolveBlueprintDocumentPath(baseDir, bpSlug);
13
27
  await updateLinkedSlugs(blueprintPath, "linked_tech_debt_slugs", (linked) => linked.filter((slug) => slug !== tdSlug));
14
28
  const tdPath = path.join(resolveTechDebtRoot(projectPath), tdSlug, "README.md");
15
29
  try {
@@ -5,6 +5,7 @@
5
5
  * platform requests share the strict origin, redirect, body, redaction, abort,
6
6
  * and retry boundary in src/platform/client.ts.
7
7
  */
8
+ import { type FetchLike } from "#platform/client.js";
8
9
  import type { SyncCredentials } from "./auth.js";
9
10
  import type { BlueprintPlatformClient, BlueprintPlatformEvent, BlueprintSnapshot, BlueprintTemplateEntry } from "./types.js";
10
11
  export declare class AuthError extends Error {
@@ -22,7 +23,7 @@ export declare class BlueprintSyncClient implements BlueprintPlatformClient {
22
23
  private readonly fetchFn;
23
24
  private platformClient;
24
25
  private readonly retry;
25
- constructor(creds: SyncCredentials, fetchFn?: typeof globalThis.fetch, retryOptions?: RetryOptions);
26
+ constructor(creds: SyncCredentials, fetchFn?: FetchLike, retryOptions?: RetryOptions);
26
27
  pushEvent(payload: BlueprintPlatformEvent): Promise<void>;
27
28
  getSnapshot(opts?: {
28
29
  readonly slug?: string;
@@ -6,7 +6,7 @@
6
6
  * and retry boundary in src/platform/client.ts.
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
- import { PlatformClient, PlatformHttpError } from "#platform/client.js";
9
+ import { PlatformClient, PlatformHttpError, } from "#platform/client.js";
10
10
  export class AuthError extends Error {
11
11
  constructor(message) {
12
12
  super(message);
@@ -0,0 +1,26 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export declare const ALLOWED_WP_COMMAND_TIMEOUT_MS = 30000;
3
+ export declare const ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT = 500;
4
+ export type AllowedWpCommandOutcome = "passed" | "failed" | "timed-out";
5
+ export interface AllowedWpCommandResult {
6
+ readonly command: string;
7
+ readonly outcome: AllowedWpCommandOutcome;
8
+ readonly durationMs: number;
9
+ readonly exitCode: number | null;
10
+ readonly signal?: string;
11
+ readonly stdout: string;
12
+ readonly stderr: string;
13
+ readonly logPath?: string;
14
+ readonly failureCode?: string;
15
+ readonly errorMessage?: string;
16
+ }
17
+ export interface ExecuteAllowedWpCommandInput {
18
+ readonly command: string;
19
+ readonly executionRoot: string;
20
+ readonly logRoot: string;
21
+ readonly env?: NodeJS.ProcessEnv;
22
+ readonly spawn?: typeof spawnSync;
23
+ }
24
+ export declare function executeAllowedWpCommand(input: ExecuteAllowedWpCommandInput): AllowedWpCommandResult;
25
+ export declare function tailBounded(text: string): string;
26
+ export declare function relativeCommandLogPath(root: string, logPath: string | undefined): string | undefined;
@@ -0,0 +1,148 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { parseAllowedWpCommand } from "./gates.js";
5
+ import { resolvePackageAssetPreferred } from "#utils/package-assets.js";
6
+ export const ALLOWED_WP_COMMAND_TIMEOUT_MS = 30_000;
7
+ export const ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT = 500;
8
+ export function executeAllowedWpCommand(input) {
9
+ const started = Date.now();
10
+ let invocation;
11
+ try {
12
+ const argv = parseAllowedWpCommand(input.command);
13
+ const [binary, ...args] = argv;
14
+ if (binary === undefined)
15
+ throw new Error(`Promotion gate command is empty: ${input.command}`);
16
+ invocation = resolveAllowedWpInvocation(input.executionRoot, binary, args, input.env);
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ const result = {
21
+ command: input.command,
22
+ outcome: "failed",
23
+ durationMs: Date.now() - started,
24
+ exitCode: null,
25
+ stdout: "",
26
+ stderr: message,
27
+ failureCode: "command-policy-rejected",
28
+ errorMessage: message,
29
+ };
30
+ return {
31
+ ...result,
32
+ logPath: writeAllowedWpCommandLog(input.logRoot, input.command, null, result),
33
+ };
34
+ }
35
+ const spawn = input.spawn ?? spawnSync;
36
+ const result = spawn(invocation.command, invocation.args, {
37
+ cwd: input.executionRoot,
38
+ encoding: "utf8",
39
+ stdio: ["ignore", "pipe", "pipe"],
40
+ timeout: ALLOWED_WP_COMMAND_TIMEOUT_MS,
41
+ env: invocation.env,
42
+ });
43
+ const durationMs = Date.now() - started;
44
+ const stdout = typeof result.stdout === "string" ? result.stdout : "";
45
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
46
+ const timedOut = result.error ? errorCode(result.error) === "ETIMEDOUT" : false;
47
+ const output = {
48
+ command: input.command,
49
+ outcome: result.status === 0 && !result.error && result.signal === null
50
+ ? "passed"
51
+ : timedOut
52
+ ? "timed-out"
53
+ : "failed",
54
+ durationMs,
55
+ exitCode: typeof result.status === "number" ? result.status : null,
56
+ ...(result.signal ? { signal: result.signal } : {}),
57
+ stdout,
58
+ stderr,
59
+ ...(timedOut ? { failureCode: "command-timed-out" } : {}),
60
+ ...(result.error ? { errorMessage: result.error.message } : {}),
61
+ };
62
+ if (output.outcome === "passed")
63
+ return output;
64
+ return {
65
+ ...output,
66
+ logPath: writeAllowedWpCommandLog(input.logRoot, input.command, invocation, output),
67
+ };
68
+ }
69
+ export function tailBounded(text) {
70
+ if (text.length <= ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT)
71
+ return text.trim();
72
+ return text.slice(-ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT).trim();
73
+ }
74
+ export function relativeCommandLogPath(root, logPath) {
75
+ if (!logPath)
76
+ return undefined;
77
+ const rel = relative(root, logPath).replace(/\\/gu, "/");
78
+ return rel === "" || rel.startsWith("../") ? logPath : rel;
79
+ }
80
+ function resolveAllowedWpInvocation(executionRoot, binary, args, extraEnv) {
81
+ const baseEnv = { ...process.env, ...extraEnv };
82
+ if (binary === "./bin/wp" || binary === "wp") {
83
+ const localLauncher = resolveRepoWpLauncher(executionRoot);
84
+ if (localLauncher !== null) {
85
+ return {
86
+ command: localLauncher,
87
+ args: [...args],
88
+ env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
89
+ };
90
+ }
91
+ if (binary === "./bin/wp") {
92
+ return {
93
+ command: join(executionRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp"),
94
+ args: [...args],
95
+ env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
96
+ };
97
+ }
98
+ const packagedLauncher = resolvePackagedWpLauncher();
99
+ return {
100
+ command: process.execPath,
101
+ args: [packagedLauncher, ...args],
102
+ env: baseEnv,
103
+ };
104
+ }
105
+ return {
106
+ command: binary,
107
+ args: [...args],
108
+ env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
109
+ };
110
+ }
111
+ function resolveRepoWpLauncher(repoRoot) {
112
+ const launcher = join(repoRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp");
113
+ return existsSync(launcher) ? launcher : null;
114
+ }
115
+ function resolvePackagedWpLauncher() {
116
+ const launcher = resolvePackageAssetPreferred([
117
+ process.platform === "win32" ? "bin/wp.cmd" : "bin/wp",
118
+ "bin/wp",
119
+ ]);
120
+ if (!existsSync(launcher)) {
121
+ throw new Error(`Promotion gate failed: packaged wp launcher is unavailable at ${launcher}`);
122
+ }
123
+ return launcher;
124
+ }
125
+ function writeAllowedWpCommandLog(logRoot, command, invocation, result) {
126
+ mkdirSync(logRoot, { recursive: true });
127
+ const logPath = join(logRoot, `${Date.now()}-wp-command.log`);
128
+ writeFileSync(logPath, [
129
+ `command: ${command}`,
130
+ `invocation: ${invocation ? `${invocation.command} ${invocation.args.join(" ")}` : "not-run"}`,
131
+ `outcome: ${result.outcome}`,
132
+ `exit: ${result.exitCode ?? "null"}`,
133
+ `signal: ${result.signal ?? "null"}`,
134
+ `timeout_ms: ${ALLOWED_WP_COMMAND_TIMEOUT_MS}`,
135
+ `error: ${result.errorMessage ?? "none"}`,
136
+ "--- stderr ---",
137
+ result.stderr,
138
+ "--- stdout ---",
139
+ result.stdout,
140
+ "",
141
+ ].join("\n"), "utf8");
142
+ return logPath;
143
+ }
144
+ function errorCode(error) {
145
+ return "code" in error && typeof error.code === "string"
146
+ ? error.code
147
+ : null;
148
+ }
@@ -22,6 +22,7 @@ export type TrustPromotionGate = {
22
22
  command: string;
23
23
  expectedOutcome: string;
24
24
  lastResult: string;
25
+ defer: string;
25
26
  };
26
27
  export type TrustDossier = {
27
28
  readiness: TrustReadinessVerdict;
@@ -36,12 +36,7 @@ export function parseTrustDossier(markdown) {
36
36
  rejectedAlternatives,
37
37
  rationale,
38
38
  }), violations);
39
- const gates = parseTable(requiredBlock(blocks, "Promotion Gates"), "Promotion Gates", ["Gate", "Command", "Expected outcome", "Last result"], ([gate = "", command = "", expectedOutcome = "", lastResult = ""]) => ({
40
- gate,
41
- command,
42
- expectedOutcome,
43
- lastResult,
44
- }), violations);
39
+ const gates = parsePromotionGatesTable(requiredBlock(blocks, "Promotion Gates"), violations);
45
40
  const residualUnknowns = requiredBlock(blocks, "Residual Unknowns").trim();
46
41
  for (const [section, block] of blocks) {
47
42
  if (containsPlaceholder(block))
@@ -126,6 +121,45 @@ function parseTable(block, section, expectedHeader, build, violations) {
126
121
  }
127
122
  return result;
128
123
  }
124
+ const PROMOTION_GATES_HEADER_LEGACY = ["Gate", "Command", "Expected outcome", "Last result"];
125
+ const PROMOTION_GATES_HEADER_WITH_DEFER = [...PROMOTION_GATES_HEADER_LEGACY, "Defer"];
126
+ // Bespoke (not parseTable()) so the header/row width can be EITHER the
127
+ // legacy 4-column shape (every Promotion Gates table committed before this
128
+ // fix, and the common case going forward) OR the 5-column shape with a
129
+ // trailing Defer column (opt-in, see TrustPromotionGate.defer). A committed
130
+ // blueprint's table must have every row match its own header's width -- a
131
+ // row can't silently be short one cell.
132
+ function parsePromotionGatesTable(block, violations) {
133
+ const section = "Promotion Gates";
134
+ const rows = block
135
+ .split("\n")
136
+ .map((line) => line.trim())
137
+ .filter((line) => line.startsWith("|") && line.endsWith("|"));
138
+ if (rows.length < 2) {
139
+ violations.push({ section, message: "missing markdown table" });
140
+ return [];
141
+ }
142
+ const header = splitRow(rows[0] ?? "").map((cell) => cell.toLowerCase());
143
+ const legacyHeader = PROMOTION_GATES_HEADER_LEGACY.map((cell) => cell.toLowerCase());
144
+ const deferHeader = PROMOTION_GATES_HEADER_WITH_DEFER.map((cell) => cell.toLowerCase());
145
+ const hasDeferColumn = header.join("|") === deferHeader.join("|");
146
+ if (!hasDeferColumn && header.join("|") !== legacyHeader.join("|")) {
147
+ violations.push({ section, message: `malformed table header for ${section}` });
148
+ return [];
149
+ }
150
+ const expectedWidth = hasDeferColumn ? 5 : 4;
151
+ const result = [];
152
+ for (const row of rows.slice(2)) {
153
+ const cells = splitRow(row);
154
+ if (cells.length !== expectedWidth) {
155
+ violations.push({ section, message: `malformed table row in ${section}` });
156
+ continue;
157
+ }
158
+ const [gate = "", command = "", expectedOutcome = "", lastResult = "", defer = ""] = cells;
159
+ result.push({ gate, command, expectedOutcome, lastResult, defer });
160
+ }
161
+ return result;
162
+ }
129
163
  function requiredBlock(blocks, key) {
130
164
  const value = blocks.get(key);
131
165
  if (value === undefined)
@@ -1 +1,8 @@
1
1
  export declare function parseAllowedWpCommand(command: string): string[];
2
+ /**
3
+ * Extracts the `--file` targets from an argv already parsed by
4
+ * `parseAllowedWpCommand`. Returns an empty array for any non-`wp test` argv.
5
+ * `parseAllowedWpCommand` guarantees `wp test` args are only `--file <target>`
6
+ * pairs, so no shell/flag validation is needed here.
7
+ */
8
+ export declare function wpTestFileTargets(argv: readonly string[]): string[];
@@ -54,3 +54,23 @@ export function parseAllowedWpCommand(command) {
54
54
  }
55
55
  throw new Error(`Unsupported promotion gate wp subcommand: ${sub ?? ""}`);
56
56
  }
57
+ /**
58
+ * Extracts the `--file` targets from an argv already parsed by
59
+ * `parseAllowedWpCommand`. Returns an empty array for any non-`wp test` argv.
60
+ * `parseAllowedWpCommand` guarantees `wp test` args are only `--file <target>`
61
+ * pairs, so no shell/flag validation is needed here.
62
+ */
63
+ export function wpTestFileTargets(argv) {
64
+ if (argv[1] !== "test")
65
+ return [];
66
+ const targets = [];
67
+ const rest = argv.slice(2);
68
+ for (let i = 0; i < rest.length; i++) {
69
+ const target = rest[i + 1];
70
+ if (rest[i] === "--file" && target !== undefined) {
71
+ targets.push(target);
72
+ i += 1;
73
+ }
74
+ }
75
+ return targets;
76
+ }
@@ -7,7 +7,15 @@ export type PromotionTrustInput = {
7
7
  markdown: string;
8
8
  now?: Date;
9
9
  };
10
+ export type PromotionTrustProof = {
11
+ readonly file: string;
12
+ readonly sourceMarkdown: string;
13
+ readonly verifiedHead: string;
14
+ readonly trustedMarkdown: string;
15
+ };
16
+ export declare function preparePromotionTrustGate(input: PromotionTrustInput): PromotionTrustProof;
10
17
  export declare function applyPromotionTrustGate(input: PromotionTrustInput): string;
18
+ export declare function assertPromotionTrustProofCurrent(input: Pick<PromotionTrustInput, "repoRoot" | "file" | "markdown">, proof: PromotionTrustProof): void;
11
19
  export declare function runPromotionCommand(repoRoot: string, command: string, deps?: {
12
20
  spawn?: typeof spawnSync;
13
21
  }): void;