@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
@@ -0,0 +1,135 @@
1
+ import { constants } from "node:fs";
2
+ import { access, lstat, mkdir, readdir, rename, rmdir } from "node:fs/promises";
3
+ import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
4
+ import { resolveBlueprintFile } from "#lifecycle/local.js";
5
+ import { BLUEPRINT_STATUSES, getBlueprintDocumentPaths, isBlueprintSlugSegment, isBlueprintStatus, } from "#utils/document-paths.js";
6
+ import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
7
+ async function exists(filePath) {
8
+ try {
9
+ await access(filePath, constants.F_OK);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ async function assertRegularFile(filePath) {
17
+ const stats = await lstat(filePath);
18
+ if (!stats.isFile() || stats.isSymbolicLink()) {
19
+ throw new Error(`Blueprint canonicalization requires a regular file: ${filePath}`);
20
+ }
21
+ }
22
+ async function directCanonicalizeLocation(projectRoot, slug) {
23
+ const root = resolveBlueprintRoot(projectRoot);
24
+ const segments = slug.split("/").filter((segment) => segment.length > 0);
25
+ const scopedState = isBlueprintStatus(segments[0]) ? segments[0] : null;
26
+ const slugSegments = scopedState === null ? segments : segments.slice(1);
27
+ const stableSlug = slugSegments.join("/");
28
+ // Validate before any filesystem probe: an unvalidated slug such as
29
+ // "../outside" would otherwise resolve paths outside the blueprint root.
30
+ if (!stableSlug || !slugSegments.every((segment) => isBlueprintSlugSegment(segment))) {
31
+ return null;
32
+ }
33
+ const states = scopedState === null ? BLUEPRINT_STATUSES : [scopedState];
34
+ for (const state of states) {
35
+ const paths = getBlueprintDocumentPaths(root, state, stableSlug);
36
+ const [flatExists, folderExists] = await Promise.all([
37
+ exists(paths.flat),
38
+ exists(paths.folder),
39
+ ]);
40
+ if (flatExists && folderExists) {
41
+ await assertRegularFile(paths.flat);
42
+ await assertRegularFile(paths.folder);
43
+ throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${paths.folder}`);
44
+ }
45
+ if (flatExists) {
46
+ return {
47
+ folderDirectory: paths.directory,
48
+ folderPath: paths.folder,
49
+ path: paths.flat,
50
+ shape: "flat",
51
+ };
52
+ }
53
+ if (folderExists) {
54
+ return {
55
+ folderDirectory: paths.directory,
56
+ folderPath: paths.folder,
57
+ path: paths.folder,
58
+ shape: "folder",
59
+ };
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ const GOVERNANCE_ENTRY_NAMES = new Set(["review-events.jsonl", "reviews.md", "review-artifacts"]);
65
+ async function assertUsableTargetDirectory(directory) {
66
+ try {
67
+ const stats = await lstat(directory);
68
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
69
+ throw new Error(`Blueprint canonicalization target is not a real directory: ${directory}`);
70
+ }
71
+ }
72
+ catch (error) {
73
+ const code = error.code;
74
+ if (code !== "ENOENT")
75
+ throw error;
76
+ await mkdir(directory, { recursive: true });
77
+ return "created";
78
+ }
79
+ // Adopting a directory that already holds governance files would turn
80
+ // previously unattributed evidence into authoritative folder evidence the
81
+ // moment the overview lands next to it. Refuse; the operator must review
82
+ // and remove (or intentionally commit) that evidence first. Compare
83
+ // case-folded: on case-insensitive filesystems "Reviews.md" aliases the
84
+ // authoritative lowercase path.
85
+ const adopted = (await readdir(directory)).filter((entry) => GOVERNANCE_ENTRY_NAMES.has(entry.toLowerCase()));
86
+ if (adopted.length > 0) {
87
+ throw new Error(`Blueprint canonicalization refused to adopt a directory containing review evidence: ${directory} (${adopted.join(", ")})`);
88
+ }
89
+ return "existing";
90
+ }
91
+ async function removeCreatedDirectoryIfEmpty(directory, created) {
92
+ if (!created)
93
+ return;
94
+ try {
95
+ const entries = await readdir(directory);
96
+ if (entries.length === 0)
97
+ await rmdir(directory);
98
+ }
99
+ catch {
100
+ // Best-effort cleanup only. A concurrent caller may have completed the move.
101
+ }
102
+ }
103
+ export async function canonicalizeBlueprint(projectRoot, slug) {
104
+ const location = (await directCanonicalizeLocation(projectRoot, slug)) ??
105
+ (await resolveBlueprintFile(projectRoot, slug));
106
+ const from = location.path;
107
+ const to = location.folderPath;
108
+ if (location.shape === "folder") {
109
+ await assertRegularFile(to);
110
+ return { from, to, moved: false };
111
+ }
112
+ await assertRegularFile(from);
113
+ const directoryState = await assertUsableTargetDirectory(location.folderDirectory);
114
+ const createdDirectory = directoryState === "created";
115
+ try {
116
+ if (await exists(to)) {
117
+ await assertRegularFile(to);
118
+ if (await exists(from)) {
119
+ throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${to}`);
120
+ }
121
+ return { from, to, moved: false };
122
+ }
123
+ await rename(from, to);
124
+ }
125
+ catch (error) {
126
+ if (error.code === "ENOENT" && (await exists(to))) {
127
+ await assertRegularFile(to);
128
+ return { from, to, moved: false };
129
+ }
130
+ await removeCreatedDirectoryIfEmpty(location.folderDirectory, createdDirectory);
131
+ throw error;
132
+ }
133
+ refreshBlueprintReadmeIndex(projectRoot);
134
+ return { from, to, moved: true };
135
+ }
@@ -1,5 +1,6 @@
1
1
  import type { BlueprintLifecycleIntent, BlueprintLifecycleResult } from "./engine.js";
2
- export interface ResolvedBlueprintFile {
2
+ import { type BlueprintDocumentIdentity } from "#utils/document-paths.js";
3
+ export interface ResolvedBlueprintFile extends BlueprintDocumentIdentity {
3
4
  path: string;
4
5
  slug: string;
5
6
  }
@@ -1,10 +1,11 @@
1
1
  import { mkdir, readFile, readdir, rename, rmdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
4
+ import { assertCompletedApprovalGate } from "#lifecycle/audit";
4
5
  import { applyBlueprintLifecycle } from "#lifecycle/engine";
5
6
  import { scanBlueprintDirectory } from "#service/scanner";
6
7
  import { resolveBlueprintRoot } from "#utils/blueprint-root";
7
- import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
8
+ import { getBlueprintDocumentPaths, resolveBlueprintDocumentIdentity, } from "#utils/document-paths.js";
8
9
  const BLUEPRINT_SLUG_SEGMENT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
10
  function isStatusSegment(segment) {
10
11
  return (segment === "draft" ||
@@ -57,7 +58,15 @@ export async function resolveBlueprintFile(projectRoot, slug) {
57
58
  .map((entry) => entry.slug)
58
59
  .join(", ")}`);
59
60
  }
60
- return { path: match.path, slug: match.slug };
61
+ const identity = resolveBlueprintDocumentIdentity(baseDir, match.path);
62
+ if (!identity) {
63
+ throw new Error(`Blueprint ${match.slug} resolved to an invalid document path: ${match.path}`);
64
+ }
65
+ return {
66
+ ...identity,
67
+ path: identity.documentPath,
68
+ slug: match.slug,
69
+ };
61
70
  }
62
71
  async function tryRemoveEmptyParent(dir) {
63
72
  try {
@@ -75,8 +84,15 @@ export async function applyBlueprintLifecycleToFile(projectRoot, slug, intent) {
75
84
  const location = await resolveBlueprintFile(projectRoot, slug);
76
85
  const raw = await readFile(location.path, "utf-8");
77
86
  const mutation = applyBlueprintLifecycle(raw, location.slug, intent);
87
+ // Completion parity: the execution engine's `finalize` intent (exec/resume/
88
+ // stop/move flows) also writes `status: completed`. Gate it with the same
89
+ // plan-approval check CI enforces so this path cannot diverge from the audit
90
+ // either — matching the MCP funnel and CLI completion owners.
91
+ if (mutation.targetStatus === "completed") {
92
+ assertCompletedApprovalGate(location.path, raw);
93
+ }
78
94
  const relativeSlug = relativeBlueprintSlug(location.slug);
79
- const isFlatFile = path.basename(location.path) !== "_overview.md";
95
+ const isFlatFile = location.shape === "flat";
80
96
  const sourceDir = path.dirname(location.path);
81
97
  const targetDocumentPaths = getBlueprintDocumentPaths(baseDir, mutation.targetStatus, relativeSlug);
82
98
  const targetDir = targetDocumentPaths.directory;
@@ -36,6 +36,12 @@ export interface TrackedReviewProvenanceOptions {
36
36
  readonly targetHash?: string;
37
37
  }
38
38
  export declare function normalizeTrackedReviewApprovals(approvals: unknown): TrackedReviewApproval[];
39
+ /**
40
+ * Slug for a blueprint file, invariant across lifecycle moves
41
+ * (`draft/ → planned/ → completed/`) and across both the folder
42
+ * (`<slug>/_overview.md`) and legacy flat (`<slug>.md`) shapes.
43
+ */
44
+ export declare function stableSlugForBlueprintFile(file: string): string;
39
45
  /**
40
46
  * Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
41
47
  * if missing (lifecycle move draft→planned before the promote commit), resolve
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
5
5
  import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
6
- import { createPlanSubjectDigestForContent, normalizePlanSubjectContent } from "#review/subject.js";
6
+ import { createDeliverySubjectAtRef, createLegacyPlanSubjectDigestForContent, createPlanSubjectDigestForContent, createPreTagsStripPlanSubjectDigestForContent, normalizePlanSubjectContent, } from "#review/subject.js";
7
7
  import { isPolicyApprovalVerdict } from "#review/verdict.js";
8
8
  const REVIEW_ENTRY_MARKER = "<!-- wp:review-entry ";
9
9
  function normalizePath(file) {
@@ -44,12 +44,21 @@ function findGitRoot(file) {
44
44
  current = parent;
45
45
  }
46
46
  }
47
- function stableSlugForBlueprintFile(file) {
47
+ /**
48
+ * Slug for a blueprint file, invariant across lifecycle moves
49
+ * (`draft/ → planned/ → completed/`) and across both the folder
50
+ * (`<slug>/_overview.md`) and legacy flat (`<slug>.md`) shapes.
51
+ */
52
+ export function stableSlugForBlueprintFile(file) {
48
53
  const segments = normalizePath(file).split("/");
49
54
  const lifecycleIndex = segments.findIndex((segment) => new Set(["draft", "planned", "parked", "in-progress", "completed", "archived"]).has(segment));
50
55
  if (lifecycleIndex >= 0 && segments.at(-1) === "_overview.md") {
51
56
  return segments.slice(lifecycleIndex + 1, -1).join("/");
52
57
  }
58
+ const basename = segments.at(-1) ?? "";
59
+ if (lifecycleIndex >= 0 && basename.endsWith(".md")) {
60
+ return basename.slice(0, -3);
61
+ }
53
62
  return path.basename(path.dirname(file));
54
63
  }
55
64
  /**
@@ -83,7 +92,7 @@ export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
83
92
  timeout: 5_000,
84
93
  });
85
94
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
86
- const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}/_overview\\.md$`, "u");
95
+ const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}(?:/_overview\\.md|\\.md)$`, "u");
87
96
  const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
88
97
  if (hits.length !== 1)
89
98
  return null;
@@ -135,21 +144,68 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
135
144
  // event still counts only when its subjectDigest matches the digest of the
136
145
  // exact working bytes being committed and its artifact hash-verifies on disk —
137
146
  // content-addressing keeps this sound before the commit exists.
138
- function collectWorkingV2ReviewProvenance(file, issues) {
147
+ function collectWorkingV2ReviewProvenance(file, purpose, issues) {
139
148
  try {
149
+ const repoRoot = findGitRoot(file);
150
+ if (!repoRoot)
151
+ return [];
140
152
  const slug = stableSlugForBlueprintFile(file);
141
153
  const working = readWorkingReviewEvents(file, slug);
142
154
  if (working.events.length === 0)
143
155
  return [];
144
156
  issues?.push(...working.issues);
145
- const digest = createPlanSubjectDigestForContent(slug, readFileSync(file));
146
- const evaluation = evaluateReviewEvents(working.events, {
147
- purpose: "plan",
157
+ // Events whose recorded `reviewedCommit` no longer resolves (typically
158
+ // orphaned by a rebase). Collected here rather than mutating
159
+ // `working.compromisedIds`, which is a ReadonlySet by contract, then merged
160
+ // into the set handed to evaluateReviewEvents below.
161
+ const unresolvableEventIds = new Set();
162
+ const subjects = purpose === "plan"
163
+ ? [
164
+ ...new Map([
165
+ createPlanSubjectDigestForContent(slug, readFileSync(file)),
166
+ createPreTagsStripPlanSubjectDigestForContent(slug, readFileSync(file)),
167
+ createLegacyPlanSubjectDigestForContent(slug, readFileSync(file)),
168
+ ].map((digest) => [digest, { scheme: "wp-blueprint-v1", digest }])).values(),
169
+ ]
170
+ : [
171
+ ...new Map(working.events
172
+ .filter((event) => event.purpose === "delivery")
173
+ .flatMap((event) => {
174
+ // Scope subject-construction failure to THIS event. A rebase
175
+ // rewrites commit SHAs, so a previously-recorded
176
+ // `reviewedCommit` can become unresolvable; letting that throw
177
+ // would escape to the outer catch and discard every approval,
178
+ // including valid ones for commits that still resolve. Mirror
179
+ // the ref-path's markCompromisedArtifactEvents: record the
180
+ // issue, mark the event compromised, and drop only it.
181
+ let subject;
182
+ try {
183
+ subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, normalizePath(path.relative(repoRoot, file)));
184
+ }
185
+ catch (error) {
186
+ issues?.push(`Working delivery review subject unavailable for ${event.id} at ${event.reviewedCommit}: ${error instanceof Error ? error.message : String(error)}`);
187
+ // Compromised (not merely skipped) so the event can never
188
+ // grant approval while still participating in reviewer
189
+ // supersession — see evaluateReviewEvents.
190
+ unresolvableEventIds.add(event.id);
191
+ return [];
192
+ }
193
+ if (subject.digest !== event.subjectDigest) {
194
+ issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
195
+ return [];
196
+ }
197
+ return [[subject.digest, subject]];
198
+ })).values(),
199
+ ];
200
+ return subjects
201
+ .flatMap((subject) => evaluateReviewEvents(working.events, {
202
+ purpose,
148
203
  blueprintSlug: slug,
149
- subjectScheme: "wp-blueprint-v1",
150
- subjectDigest: digest,
151
- }, working.compromisedIds);
152
- return evaluation.approvals
204
+ subjectScheme: subject.scheme,
205
+ subjectDigest: subject.digest,
206
+ }, unresolvableEventIds.size === 0
207
+ ? working.compromisedIds
208
+ : new Set([...working.compromisedIds, ...unresolvableEventIds])).approvals)
153
209
  .filter((event) => event.artifactPath)
154
210
  .map((event) => ({
155
211
  reviewer: event.reviewer,
@@ -166,7 +222,7 @@ export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
166
222
  const repoRoot = findGitRoot(file);
167
223
  if (!repoRoot)
168
224
  return [];
169
- const workingApprovals = purpose === "plan" ? collectWorkingV2ReviewProvenance(file, issues) : [];
225
+ const workingApprovals = collectWorkingV2ReviewProvenance(file, purpose, issues);
170
226
  if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
171
227
  return workingApprovals;
172
228
  try {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Rollout grandfathering, decided from git history instead of frontmatter.
3
+ *
4
+ * Two gates used to grant themselves an exemption by reading a blueprint's own
5
+ * `created:` frontmatter and comparing it to a hardcoded rollout date. That
6
+ * value is written by the same author the gate is meant to constrain, so
7
+ * `created: "2020-01-01"` disabled the check outright — on the CI audit path and
8
+ * (for the approval gate) on the `assertCompletedApprovalGate` write path, which
9
+ * would let a blueprint reach `completed` carrying no approval at all.
10
+ *
11
+ * Presence in a fixed commit's tree is instead a structural fact about history:
12
+ * no file content can add a slug to a tree that was written months ago.
13
+ *
14
+ * Matching on **slug** rather than path is deliberate. Blueprints legitimately
15
+ * move `draft/ → planned/ → completed/`, so a path-based check would mistake a
16
+ * lifecycle move for a new blueprint, while `git log --follow` would introduce a
17
+ * rename-similarity heuristic. The slug is invariant across those moves — the
18
+ * repo already enforces one blueprint = one slug = one `bp/<slug>` branch.
19
+ */
20
+ /**
21
+ * The commit that introduced both the completed-approval gate and the Trust
22
+ * Dossier requirement ("fix: preserve pre-contract blueprint rollouts",
23
+ * 2026-07-12). Blueprints already tracked at this commit predate both contracts.
24
+ */
25
+ export declare const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
26
+ /** The date that anchor landed, used only by the legacy fallback below. */
27
+ export declare const ROLLOUT_DATE = "2026-07-12";
28
+ /**
29
+ * Outcome of consulting the anchor, cached per (repoRoot, anchorCommit).
30
+ *
31
+ * - `not-applicable`: the anchor commit is not in this repository at all. The
32
+ * rollout it marks is an agent-kit history event, so in a consumer repo (which
33
+ * has its own unrelated history) the "predates the rollout" question is
34
+ * vacuous. Those repos keep their prior behaviour: nothing is retroactively
35
+ * gated. The security property is unaffected where it matters, because in
36
+ * agent-kit the anchor is an ancestor of `main`. Note a shallow clone can
37
+ * also lack it; that case is detected separately and fails closed rather
38
+ * than falling back, so truncated history cannot reopen the bypass.
39
+ * - `slugs`: the anchor resolved; membership decides the exemption.
40
+ * - `unreadable`: the anchor exists but could not be listed, OR it is absent
41
+ * from a shallow clone where truncated history could be hiding it. Both are
42
+ * inconclusive rather than different-repo signals, so both fail **closed**.
43
+ * Memoised for the process lifetime, so a transient git failure stays
44
+ * fail-closed for the rest of the run -- acceptable for short-lived CLI and
45
+ * audit processes, and the safe direction if it ever is not.
46
+ */
47
+ type AnchorLookup = {
48
+ readonly kind: "not-applicable";
49
+ } | {
50
+ readonly kind: "slugs";
51
+ readonly slugs: ReadonlySet<string>;
52
+ } | {
53
+ readonly kind: "unreadable";
54
+ };
55
+ /**
56
+ * Slugs tracked under `blueprints/` at `anchorCommit`, or `null` when the anchor
57
+ * cannot be read. Memoised per (repoRoot, anchorCommit).
58
+ *
59
+ * Callers must treat `null` as "grandfather nobody" — see
60
+ * {@link isBlueprintExemptFromRollout}, which fails closed.
61
+ */
62
+ export declare function blueprintSlugsAtRolloutAnchor(repoRoot: string, anchorCommit?: string): AnchorLookup;
63
+ /**
64
+ * True when this blueprint is exempt from a rollout-gated contract.
65
+ *
66
+ * Resolution order, deliberately:
67
+ *
68
+ * 1. **Anchor is present in this repository** (the agent-kit repo, where these
69
+ * contracts actually rolled out and where CI and the promote/finalize write
70
+ * path enforce them): membership in the anchor tree decides, and the
71
+ * author-supplied `created` is ignored entirely. This is what closes the
72
+ * forgery — a blueprint cannot add its own slug to a months-old tree.
73
+ * 2. **Anchor absent from a full (non-shallow) clone, or no git repository at
74
+ * all**: the rollout is an agent-kit history event that never happened here,
75
+ * so the anchor cannot answer the question. Fall back to the previous
76
+ * `created` compare. This is unchanged prior behaviour for consumer repos and
77
+ * non-git contexts, which is deliberate: those contexts have no such
78
+ * protection today, so tightening them is a separate decision, and gating
79
+ * them here would be unsatisfiable anyway because every accepted form of
80
+ * approval provenance is git-tracked.
81
+ * 3. **Inconclusive**: the anchor exists but cannot be listed, or it is absent
82
+ * from a *shallow* clone where truncated history could be hiding it. Fails
83
+ * **closed** — the gate applies. Without the shallow check, `git clone
84
+ * --depth 1` of this very repository would drop to the forgeable date
85
+ * fallback and reopen the bypass on the local promote/finalize write path.
86
+ *
87
+ * Net effect: strictly hardens the gate where it is enforced, and regresses
88
+ * nothing anywhere else.
89
+ */
90
+ export declare function isBlueprintExemptFromRollout(input: {
91
+ readonly repoRoot: string | null | undefined;
92
+ readonly slug: string;
93
+ readonly created: unknown;
94
+ readonly rolloutDate: string;
95
+ readonly anchorCommit?: string;
96
+ }): boolean;
97
+ export {};
@@ -0,0 +1,161 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /**
3
+ * Rollout grandfathering, decided from git history instead of frontmatter.
4
+ *
5
+ * Two gates used to grant themselves an exemption by reading a blueprint's own
6
+ * `created:` frontmatter and comparing it to a hardcoded rollout date. That
7
+ * value is written by the same author the gate is meant to constrain, so
8
+ * `created: "2020-01-01"` disabled the check outright — on the CI audit path and
9
+ * (for the approval gate) on the `assertCompletedApprovalGate` write path, which
10
+ * would let a blueprint reach `completed` carrying no approval at all.
11
+ *
12
+ * Presence in a fixed commit's tree is instead a structural fact about history:
13
+ * no file content can add a slug to a tree that was written months ago.
14
+ *
15
+ * Matching on **slug** rather than path is deliberate. Blueprints legitimately
16
+ * move `draft/ → planned/ → completed/`, so a path-based check would mistake a
17
+ * lifecycle move for a new blueprint, while `git log --follow` would introduce a
18
+ * rename-similarity heuristic. The slug is invariant across those moves — the
19
+ * repo already enforces one blueprint = one slug = one `bp/<slug>` branch.
20
+ */
21
+ /**
22
+ * The commit that introduced both the completed-approval gate and the Trust
23
+ * Dossier requirement ("fix: preserve pre-contract blueprint rollouts",
24
+ * 2026-07-12). Blueprints already tracked at this commit predate both contracts.
25
+ */
26
+ export const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
27
+ /** The date that anchor landed, used only by the legacy fallback below. */
28
+ export const ROLLOUT_DATE = "2026-07-12";
29
+ const anchorSlugCache = new Map();
30
+ function anchorCommitExists(repoRoot, anchorCommit) {
31
+ try {
32
+ execFileSync("git", ["cat-file", "-e", `${anchorCommit}^{commit}`], {
33
+ cwd: repoRoot,
34
+ stdio: ["ignore", "ignore", "ignore"],
35
+ });
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * A shallow clone (`git clone --depth 1`) can be missing the anchor purely
44
+ * because history was truncated, which is indistinguishable from "different
45
+ * repository" by anchor lookup alone. Detecting shallowness lets us tell the two
46
+ * apart instead of assuming the benign one.
47
+ */
48
+ function isShallowRepository(repoRoot) {
49
+ try {
50
+ const out = execFileSync("git", ["rev-parse", "--is-shallow-repository"], {
51
+ cwd: repoRoot,
52
+ encoding: "utf8",
53
+ stdio: ["ignore", "pipe", "ignore"],
54
+ });
55
+ // A path that is not a git repository at all answers with a non-zero exit,
56
+ // handled below. Anything else that is not a clean "false" is treated as
57
+ // truncated, so an odd repository shape fails closed rather than falling
58
+ // back to the forgeable date.
59
+ return out.trim() !== "false";
60
+ }
61
+ catch {
62
+ // Not a git repository (or git is unusable here). This is the "no history to
63
+ // consult" case, not a truncated-history case: the caller falls back, which
64
+ // preserves prior behaviour for non-git contexts where the gate would be
65
+ // unsatisfiable anyway.
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Slugs tracked under `blueprints/` at `anchorCommit`, or `null` when the anchor
71
+ * cannot be read. Memoised per (repoRoot, anchorCommit).
72
+ *
73
+ * Callers must treat `null` as "grandfather nobody" — see
74
+ * {@link isBlueprintExemptFromRollout}, which fails closed.
75
+ */
76
+ export function blueprintSlugsAtRolloutAnchor(repoRoot, anchorCommit = ROLLOUT_ANCHOR_COMMIT) {
77
+ const cacheKey = `${anchorCommit}:${repoRoot}`;
78
+ const cached = anchorSlugCache.get(cacheKey);
79
+ if (cached !== undefined)
80
+ return cached;
81
+ const result = (() => {
82
+ if (!anchorCommitExists(repoRoot, anchorCommit)) {
83
+ // Absent because this is a different repository (fine -- the rollout never
84
+ // happened here) or because history was truncated (NOT fine -- that would
85
+ // silently reopen the forged-date bypass). Only the former may fall back.
86
+ return isShallowRepository(repoRoot) ? { kind: "unreadable" } : { kind: "not-applicable" };
87
+ }
88
+ try {
89
+ const stdout = execFileSync("git", ["ls-tree", "-r", "--name-only", anchorCommit, "blueprints/"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
90
+ const found = new Set();
91
+ for (const line of stdout.split("\n")) {
92
+ const match = /^blueprints\/[^/]+\/(.+)$/u.exec(line.trim());
93
+ if (!match)
94
+ continue;
95
+ const rest = match[1] ?? "";
96
+ // Folder shape `<slug>/_overview.md`, or legacy flat `<slug>.md`.
97
+ const slug = rest.includes("/")
98
+ ? rest.slice(0, rest.indexOf("/"))
99
+ : rest.endsWith(".md")
100
+ ? rest.slice(0, -3)
101
+ : "";
102
+ if (slug)
103
+ found.add(slug);
104
+ }
105
+ return { kind: "slugs", slugs: found };
106
+ }
107
+ catch {
108
+ return { kind: "unreadable" };
109
+ }
110
+ })();
111
+ anchorSlugCache.set(cacheKey, result);
112
+ return result;
113
+ }
114
+ /** Legacy fallback: the author-supplied `created` date compare. */
115
+ function createdPredatesRollout(created, rolloutDate) {
116
+ const date = created instanceof Date
117
+ ? created.toISOString().slice(0, 10)
118
+ : typeof created === "string" && /^\d{4}-\d{2}-\d{2}$/u.test(created)
119
+ ? created
120
+ : null;
121
+ return date !== null && date < rolloutDate;
122
+ }
123
+ /**
124
+ * True when this blueprint is exempt from a rollout-gated contract.
125
+ *
126
+ * Resolution order, deliberately:
127
+ *
128
+ * 1. **Anchor is present in this repository** (the agent-kit repo, where these
129
+ * contracts actually rolled out and where CI and the promote/finalize write
130
+ * path enforce them): membership in the anchor tree decides, and the
131
+ * author-supplied `created` is ignored entirely. This is what closes the
132
+ * forgery — a blueprint cannot add its own slug to a months-old tree.
133
+ * 2. **Anchor absent from a full (non-shallow) clone, or no git repository at
134
+ * all**: the rollout is an agent-kit history event that never happened here,
135
+ * so the anchor cannot answer the question. Fall back to the previous
136
+ * `created` compare. This is unchanged prior behaviour for consumer repos and
137
+ * non-git contexts, which is deliberate: those contexts have no such
138
+ * protection today, so tightening them is a separate decision, and gating
139
+ * them here would be unsatisfiable anyway because every accepted form of
140
+ * approval provenance is git-tracked.
141
+ * 3. **Inconclusive**: the anchor exists but cannot be listed, or it is absent
142
+ * from a *shallow* clone where truncated history could be hiding it. Fails
143
+ * **closed** — the gate applies. Without the shallow check, `git clone
144
+ * --depth 1` of this very repository would drop to the forgeable date
145
+ * fallback and reopen the bypass on the local promote/finalize write path.
146
+ *
147
+ * Net effect: strictly hardens the gate where it is enforced, and regresses
148
+ * nothing anywhere else.
149
+ */
150
+ export function isBlueprintExemptFromRollout(input) {
151
+ const { repoRoot, slug, created, rolloutDate } = input;
152
+ const anchorCommit = input.anchorCommit ?? ROLLOUT_ANCHOR_COMMIT;
153
+ if (repoRoot) {
154
+ const lookup = blueprintSlugsAtRolloutAnchor(repoRoot, anchorCommit);
155
+ if (lookup.kind === "slugs")
156
+ return slug.length > 0 && lookup.slugs.has(slug);
157
+ if (lookup.kind === "unreadable")
158
+ return false;
159
+ }
160
+ return createdPredatesRollout(created, rolloutDate);
161
+ }
@@ -48,6 +48,7 @@ export declare class BlueprintCreationService {
48
48
  resolveDraftSlug(input: CreateBlueprintDraftInput): string;
49
49
  compileDraft(input: CreateBlueprintDraftInput): Promise<CompiledBlueprintDraft>;
50
50
  create(input: CreateBlueprintDraftInput): Promise<CreatedBlueprintDraft>;
51
+ private reserveDraftSlugFolder;
51
52
  private resolveCollisionSafeSlug;
52
53
  }
53
54
  export {};