@webpresso/agent-kit 3.1.28 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (329) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +179 -19
  3. package/catalog/AGENTS.md.tpl +6 -6
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  6. package/catalog/agent/rules/ci-test-perf.md +5 -1
  7. package/catalog/agent/rules/cmd-execution.md +9 -0
  8. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  9. package/catalog/agent/rules/pre-implementation.md +30 -2
  10. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  11. package/catalog/agent/rules/test-scan-perf.md +80 -0
  12. package/catalog/agent/rules/typescript-version.md +50 -0
  13. package/catalog/agent/rules/workflow-skills-routing.md +4 -2
  14. package/catalog/agent/skills/codex/SKILL.md +25 -1
  15. package/catalog/agent/skills/fix/SKILL.md +3 -0
  16. package/catalog/agent/skills/verify/SKILL.md +22 -2
  17. package/catalog/docs/templates/blueprint.md +1 -1
  18. package/dist/esm/audit/agents.js +1 -0
  19. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  20. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  21. package/dist/esm/audit/blueprint-pr-coverage.js +72 -7
  22. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
  24. package/dist/esm/audit/ci-test-perf.js +30 -0
  25. package/dist/esm/audit/command-surface.js +4 -2
  26. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  27. package/dist/esm/audit/host-substitution-risk.js +256 -0
  28. package/dist/esm/audit/registry.d.ts +4 -4
  29. package/dist/esm/audit/registry.js +11 -1
  30. package/dist/esm/audit/roadmap-links.js +6 -0
  31. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  32. package/dist/esm/audit/test-scan-perf.js +275 -0
  33. package/dist/esm/audit/toolchain-isolation.js +1 -2
  34. package/dist/esm/audit/typescript-version.d.ts +2 -0
  35. package/dist/esm/audit/typescript-version.js +123 -0
  36. package/dist/esm/blueprint/core/parser.js +43 -1
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +65 -10
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  46. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  47. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  48. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  49. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  50. package/dist/esm/blueprint/lifecycle/review-provenance.js +118 -12
  51. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  52. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  53. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  54. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  55. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  56. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  57. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  58. package/dist/esm/blueprint/trust/gates.js +20 -0
  59. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  60. package/dist/esm/blueprint/trust/promotion.js +62 -112
  61. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  62. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  63. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  64. package/dist/esm/blueprint/trust/validator.js +7 -1
  65. package/dist/esm/blueprint/utils/archive.js +27 -5
  66. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  67. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  68. package/dist/esm/build/cli-mcp-parity.js +10 -0
  69. package/dist/esm/ci/act-replay.js +110 -7
  70. package/dist/esm/ci/act-runner.d.ts +4 -0
  71. package/dist/esm/ci/act-runner.js +85 -26
  72. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  73. package/dist/esm/ci/act-worktree-git.js +219 -0
  74. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  75. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  76. package/dist/esm/cli/cli.d.ts +1 -1
  77. package/dist/esm/cli/cli.js +34 -0
  78. package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
  79. package/dist/esm/cli/commands/agent-launch.js +82 -4
  80. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  81. package/dist/esm/cli/commands/audit-core.js +8 -1
  82. package/dist/esm/cli/commands/audit.js +20 -5
  83. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  84. package/dist/esm/cli/commands/blueprint/mutations.js +83 -47
  85. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  86. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  87. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  88. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  89. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  90. package/dist/esm/cli/commands/blueprint/router.js +40 -3
  91. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  92. package/dist/esm/cli/commands/ci-preflight.js +6 -3
  93. package/dist/esm/cli/commands/ci.d.ts +8 -1
  94. package/dist/esm/cli/commands/ci.js +80 -21
  95. package/dist/esm/cli/commands/compile.js +5 -31
  96. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  97. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  98. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  99. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  100. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  101. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  102. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  103. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  104. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  105. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  106. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  107. package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
  108. package/dist/esm/cli/commands/dash/plan-once.js +41 -4
  109. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  110. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  111. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  112. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  113. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  114. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  115. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  116. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  117. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  118. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  119. package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
  120. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  121. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  122. package/dist/esm/cli/commands/doctor.js +5 -0
  123. package/dist/esm/cli/commands/format.js +0 -2
  124. package/dist/esm/cli/commands/hook.d.ts +5 -0
  125. package/dist/esm/cli/commands/hook.js +15 -9
  126. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  127. package/dist/esm/cli/commands/init/config.js +3 -6
  128. package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
  129. package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
  130. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  131. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  132. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  133. package/dist/esm/cli/commands/init/convergence.js +57 -0
  134. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  135. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  136. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  137. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  138. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  139. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  140. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  141. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  142. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  143. package/dist/esm/cli/commands/init/index.d.ts +1 -0
  144. package/dist/esm/cli/commands/init/index.js +326 -173
  145. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  146. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  147. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  148. package/dist/esm/cli/commands/init/merge.js +30 -1
  149. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  150. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  151. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  152. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  153. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
  154. package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
  155. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  156. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  157. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  158. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  159. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  160. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
  161. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  162. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  163. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  164. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  165. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  166. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +297 -173
  167. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
  168. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
  169. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  170. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  171. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  172. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  173. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  174. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  175. package/dist/esm/cli/commands/logs.d.ts +15 -0
  176. package/dist/esm/cli/commands/logs.js +192 -3
  177. package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
  178. package/dist/esm/cli/commands/opencode-rotate.js +135 -0
  179. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  180. package/dist/esm/cli/commands/public-readiness.js +42 -0
  181. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  182. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  183. package/dist/esm/cli/commands/review.d.ts +35 -3
  184. package/dist/esm/cli/commands/review.js +734 -217
  185. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  186. package/dist/esm/cli/commands/run-wait.js +122 -0
  187. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  188. package/dist/esm/cli/commands/secrets.js +4 -1
  189. package/dist/esm/cli/commands/setup-help.js +1 -1
  190. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  191. package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
  192. package/dist/esm/cli/direct-provider-launch.js +31 -3
  193. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  194. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  195. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  196. package/dist/esm/daemon/domains/git-actions.js +136 -0
  197. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  198. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  199. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  200. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  201. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  202. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  203. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  204. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  205. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  206. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  207. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  208. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  209. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  210. package/dist/esm/daemon/handlers/product.js +10 -119
  211. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  212. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  213. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  214. package/dist/esm/daemon/handlers/shared.js +16 -0
  215. package/dist/esm/daemon/methods.d.ts +8 -1
  216. package/dist/esm/daemon/methods.js +7 -0
  217. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  218. package/dist/esm/daemon/protocol/contract.js +26 -0
  219. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  220. package/dist/esm/daemon/protocol/index.js +1 -1
  221. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  222. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  223. package/dist/esm/hooks/conformance/batch.js +5 -0
  224. package/dist/esm/hooks/doctor.d.ts +15 -0
  225. package/dist/esm/hooks/doctor.js +79 -8
  226. package/dist/esm/hooks/permission-request/index.js +9 -3
  227. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  228. package/dist/esm/hooks/permission-request/policy.js +13 -0
  229. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  230. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  231. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  232. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  233. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  234. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  235. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  236. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +58 -4
  237. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  238. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  239. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  240. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  241. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  242. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  243. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  244. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  245. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  246. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  247. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  248. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  249. package/dist/esm/hooks/shared/types.d.ts +8 -5
  250. package/dist/esm/hooks/shared/types.js +23 -8
  251. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  252. package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
  253. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  254. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
  255. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  256. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  257. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  258. package/dist/esm/mcp/blueprint/registration.js +3 -2
  259. package/dist/esm/mcp/server.d.ts +1 -0
  260. package/dist/esm/mcp/server.js +27 -0
  261. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  262. package/dist/esm/mcp/tools/_names.js +5 -0
  263. package/dist/esm/mcp/tools/_registry.js +10 -0
  264. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  265. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  266. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  267. package/dist/esm/mcp/tools/audit.js +40 -0
  268. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  269. package/dist/esm/mcp/tools/ci-act.js +9 -11
  270. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  271. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  272. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  273. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  274. package/dist/esm/mcp/tools/review-run.js +216 -0
  275. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  276. package/dist/esm/mcp/tools/run-wait.js +593 -0
  277. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  278. package/dist/esm/mcp/tools/session-id.js +45 -0
  279. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  280. package/dist/esm/mcp/tools/session-info.js +49 -0
  281. package/dist/esm/output-transforms/audit.d.ts +23 -0
  282. package/dist/esm/output-transforms/audit.js +47 -0
  283. package/dist/esm/output-transforms/index.js +2 -0
  284. package/dist/esm/paths/state-root.js +3 -0
  285. package/dist/esm/review/authority.js +30 -7
  286. package/dist/esm/review/availability.d.ts +22 -0
  287. package/dist/esm/review/availability.js +123 -23
  288. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  289. package/dist/esm/review/delivery-verifier.js +115 -0
  290. package/dist/esm/review/events.d.ts +3 -2
  291. package/dist/esm/review/events.js +23 -3
  292. package/dist/esm/review/execution/adapters.js +26 -9
  293. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  294. package/dist/esm/review/execution/output-policy.js +12 -0
  295. package/dist/esm/review/execution/review-checkout.js +87 -6
  296. package/dist/esm/review/execution/supervisor.js +5 -0
  297. package/dist/esm/review/execution/types.d.ts +8 -0
  298. package/dist/esm/review/opencode-account.d.ts +24 -0
  299. package/dist/esm/review/opencode-account.js +68 -0
  300. package/dist/esm/review/opencode-auth.d.ts +33 -0
  301. package/dist/esm/review/opencode-auth.js +79 -0
  302. package/dist/esm/review/subject.d.ts +27 -0
  303. package/dist/esm/review/subject.js +179 -32
  304. package/dist/esm/runtime/executor.d.ts +1 -0
  305. package/dist/esm/runtime/executor.js +2 -1
  306. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  307. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  308. package/dist/esm/secret-gate/runner.d.ts +1 -0
  309. package/dist/esm/secret-gate/runner.js +4 -1
  310. package/dist/esm/status/snapshot.js +30 -19
  311. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  312. package/dist/esm/test/duration-sequencer.js +54 -0
  313. package/dist/esm/test/shard-durations.json +922 -0
  314. package/dist/esm/test/worker-budget.d.ts +6 -0
  315. package/dist/esm/test/worker-budget.js +59 -1
  316. package/dist/esm/test-helpers/global-setup.d.ts +1 -1
  317. package/dist/esm/test-helpers/global-setup.js +68 -24
  318. package/dist/esm/ultragoal/service.d.ts +7 -0
  319. package/dist/esm/ultragoal/service.js +42 -2
  320. package/dist/esm/utils/write-json-file.js +3 -1
  321. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  322. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  323. package/dist/esm/worktrees/current.d.ts +1 -0
  324. package/dist/esm/worktrees/current.js +1 -0
  325. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  326. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  327. package/dist/esm/worktrees/registry.d.ts +8 -0
  328. package/dist/esm/worktrees/registry.js +18 -3
  329. package/package.json +18 -15
@@ -39,6 +39,43 @@ export function resolveReviewCommit(cwd, ref) {
39
39
  }
40
40
  return commit;
41
41
  }
42
+ /**
43
+ * Repo-relative paths with uncommitted changes (staged, unstaged, or
44
+ * untracked) that are NOT review evidence (reviews.md / review-events.jsonl /
45
+ * review-artifacts/*.md — see isReviewEvidenceRelativePath). A `wp-git-tree-v1`
46
+ * subject digest is computed purely from the committed tree at a ref, so any
47
+ * non-evidence path here means the working tree diverges from what that
48
+ * digest — and any approval bound to it — actually covers. Review evidence is
49
+ * excluded because the gate's own artifact/ledger writes routinely leave the
50
+ * tree dirty between reruns without representing unreviewed content.
51
+ */
52
+ export function findUnreviewedWorkingTreeChanges(cwd) {
53
+ const output = gitBuffer(cwd, [
54
+ "status",
55
+ "--porcelain=v1",
56
+ "--untracked-files=all",
57
+ "--ignored=no",
58
+ "-z",
59
+ ]).toString("utf8");
60
+ const records = output.split("\0").filter((record) => record.length > 0);
61
+ const paths = new Set();
62
+ for (let index = 0; index < records.length; index += 1) {
63
+ const record = records[index];
64
+ const statusCode = record.slice(0, 2);
65
+ paths.add(record.slice(3));
66
+ if (/[RC]/u.test(statusCode)) {
67
+ // Rename/copy porcelain records are followed by the original path.
68
+ const originalPath = records[index + 1];
69
+ if (originalPath !== undefined) {
70
+ paths.add(originalPath);
71
+ index += 1;
72
+ }
73
+ }
74
+ }
75
+ return [...paths]
76
+ .filter((relativePath) => !isReviewEvidenceRelativePath(relativePath))
77
+ .toSorted();
78
+ }
42
79
  function parseTree(cwd, commit) {
43
80
  const output = gitBuffer(cwd, ["ls-tree", "-r", "-z", "--full-tree", commit]);
44
81
  const entries = [];
@@ -74,31 +111,40 @@ function parseTree(cwd, commit) {
74
111
  throw new Error("Truncated git ls-tree output.");
75
112
  return entries.sort((left, right) => Buffer.compare(left.path, right.path));
76
113
  }
77
- function findBlueprintRoot(entries, blueprintSlug, blueprintPath) {
114
+ function isRegularBlob(entry) {
115
+ return entry.type === "blob" && /^100(?:644|755)$/u.test(entry.mode.toString("ascii"));
116
+ }
117
+ function findBlueprintDocument(entries, blueprintSlug, blueprintPath) {
78
118
  if (blueprintPath) {
79
119
  const normalized = blueprintPath.replaceAll("\\", "/");
80
120
  const target = Buffer.from(normalized, "utf8");
81
- const matches = entries.filter((entry) => entry.path.equals(target));
82
- if (matches.length === 1 && normalized.endsWith("/_overview.md")) {
83
- return target.subarray(0, target.length - Buffer.byteLength("_overview.md"));
121
+ const matches = entries.filter((entry) => entry.path.equals(target) && isRegularBlob(entry));
122
+ if (matches.length === 1 && normalized.endsWith(".md")) {
123
+ return matches[0];
84
124
  }
85
125
  }
86
- const suffix = Buffer.from(`/${blueprintSlug}/_overview.md`, "utf8");
126
+ const folderSuffix = `/${blueprintSlug}/_overview.md`;
127
+ const flatSuffix = `/${blueprintSlug}.md`;
87
128
  const matches = entries.filter((entry) => {
88
129
  const normalized = entry.path.toString("utf8");
89
- const segments = normalized.split("/");
90
- return (segments[0] === "blueprints" &&
91
- BLUEPRINT_STATUSES.has(segments[1] ?? "") &&
92
- entry.path.subarray(-suffix.length).equals(suffix));
130
+ const suffix = normalized.endsWith(folderSuffix)
131
+ ? folderSuffix
132
+ : normalized.endsWith(flatSuffix)
133
+ ? flatSuffix
134
+ : undefined;
135
+ const lifecycle = suffix ? normalized.slice(0, -suffix.length).split("/").at(-1) : undefined;
136
+ return isRegularBlob(entry) && BLUEPRINT_STATUSES.has(lifecycle ?? "");
93
137
  });
94
138
  if (matches.length !== 1) {
95
139
  throw new Error(`Expected one tracked blueprint ${blueprintSlug} at target commit, found ${matches.length}.`);
96
140
  }
97
- const overview = matches[0].path;
98
- return overview.subarray(0, overview.length - Buffer.byteLength("_overview.md"));
141
+ return matches[0];
99
142
  }
100
- function isRegularBlob(entry) {
101
- return entry.type === "blob" && /^100(?:644|755)$/u.test(entry.mode.toString("ascii"));
143
+ function blueprintEvidenceRoot(documentPath) {
144
+ const value = documentPath.toString("utf8");
145
+ if (!value.endsWith("/_overview.md"))
146
+ return null;
147
+ return documentPath.subarray(0, documentPath.length - Buffer.byteLength("_overview.md"));
102
148
  }
103
149
  function isExcludedReviewEnvelope(entry, blueprintRoot) {
104
150
  if (!isRegularBlob(entry) ||
@@ -114,10 +160,10 @@ function isExcludedReviewEnvelope(entry, blueprintRoot) {
114
160
  const artifactName = relative.slice(prefix.length);
115
161
  return artifactName.length > 3 && !artifactName.includes("/") && artifactName.endsWith(".md");
116
162
  }
117
- function isReviewEvidencePath(entry) {
118
- if (!isRegularBlob(entry))
119
- return false;
120
- const value = entry.path.toString("utf8");
163
+ // String-based counterpart of isReviewEvidencePath, shared with callers that
164
+ // only have a repo-relative path (no TreeEntry) — currently the working-tree
165
+ // divergence check in findUnreviewedWorkingTreeChanges below.
166
+ export function isReviewEvidenceRelativePath(value) {
121
167
  if (!/^blueprints\/(?:[^/]+\/)*[^/]+\//u.test(value))
122
168
  return false;
123
169
  if (/(?:^|\/)reviews\.md$/u.test(value) || /(?:^|\/)review-events\.jsonl$/u.test(value)) {
@@ -125,6 +171,11 @@ function isReviewEvidencePath(entry) {
125
171
  }
126
172
  return /(?:^|\/)review-artifacts\/[^/]+\.md$/u.test(value);
127
173
  }
174
+ function isReviewEvidencePath(entry) {
175
+ if (!isRegularBlob(entry))
176
+ return false;
177
+ return isReviewEvidenceRelativePath(entry.path.toString("utf8"));
178
+ }
128
179
  function loadBlobContents(cwd, entries) {
129
180
  const blobs = entries.filter((entry) => entry.type === "blob");
130
181
  if (blobs.length === 0)
@@ -195,11 +246,13 @@ function readTreeAtRef(cwd, ref) {
195
246
  }
196
247
  export function createDeliverySubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
197
248
  const { commit, objectFormat, entries: allEntries } = readTreeAtRef(cwd, ref);
198
- const blueprintRoot = findBlueprintRoot(allEntries, blueprintSlug, blueprintPath);
249
+ const document = findBlueprintDocument(allEntries, blueprintSlug, blueprintPath);
250
+ const blueprintRoot = blueprintEvidenceRoot(document.path);
199
251
  // Exclude review evidence for the owning blueprint AND for every other blueprint
200
252
  // (the same global exclusion source snapshots use): recording evidence under an
201
253
  // unrelated blueprint must not invalidate an otherwise-identical delivery review.
202
- const entries = allEntries.filter((entry) => !isExcludedReviewEnvelope(entry, blueprintRoot) && !isReviewEvidencePath(entry));
254
+ const entries = allEntries.filter((entry) => (blueprintRoot === null || !isExcludedReviewEnvelope(entry, blueprintRoot)) &&
255
+ !isReviewEvidencePath(entry));
203
256
  return {
204
257
  scheme: "wp-git-tree-v1",
205
258
  digest: hashTreeEntries(cwd, entries, objectFormat, DELIVERY_DOMAIN),
@@ -255,12 +308,39 @@ function neutralizePromotionGateResults(text) {
255
308
  }
256
309
  return lines.join("\n");
257
310
  }
258
- // Neutralize the lifecycle-owned frontmatter fields (`status:` value and the
259
- // `completed_at:` line) INSIDE the leading frontmatter block only. A body line
311
+ // Frontmatter keys removed wholesale by the normalizer (value + folded
312
+ // continuation lines). Each mirrors exactly one lifecycle writer:
313
+ // `completed_at` (upsertCompletedAt) and the `wp blueprint start` /
314
+ // progress-stamp fields (setBlueprintFrontmatterFields lifecycle intents).
315
+ const PLAN_LIFECYCLE_STRIPPED_KEYS = [
316
+ "completed_at",
317
+ "last_updated",
318
+ "progress",
319
+ "worktree_owner_id",
320
+ "worktree_owner_branch",
321
+ ];
322
+ // Neutralize the lifecycle-owned frontmatter fields (`status:` value plus the
323
+ // stripped keys above) INSIDE the leading frontmatter block only. A body line
260
324
  // that merely looks like one of these (e.g. task text reading
261
325
  // "completed_at: must remain unset until release") is plan substance and must
262
- // keep affecting the digest.
326
+ // keep affecting the digest. Stripping removes the key line and any indented
327
+ // continuation lines so folded (`>-`) values normalize away too.
263
328
  function neutralizeLifecycleFrontmatter(text) {
329
+ const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
330
+ if (!match)
331
+ return text;
332
+ let frontmatter = match[0];
333
+ frontmatter = frontmatter.replace(/^(status:\s*)(['"]?)[^'"\r\n]+?(['"]?)(\s*)$/m, `$1${PLAN_STATUS_PLACEHOLDER}$4`);
334
+ for (const key of PLAN_LIFECYCLE_STRIPPED_KEYS) {
335
+ frontmatter = frontmatter.replace(new RegExp(String.raw `^${key}:[^\n]*\n?(?:[ \t]+[^\n]*\n?)*`, "gmu"), "");
336
+ }
337
+ return `${frontmatter}${text.slice(match[0].length)}`;
338
+ }
339
+ // The pre-extension normalizer (status + completed_at only). Review events
340
+ // recorded before the stripped-key extension bound their subjectDigest to this
341
+ // normalization; verification accepts either digest so committed approvals are
342
+ // not stranded by the rollout. New events always record the current digest.
343
+ function legacyNeutralizeLifecycleFrontmatter(text) {
264
344
  const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
265
345
  if (!match)
266
346
  return text;
@@ -305,26 +385,89 @@ function neutralizeReadinessStamps(text) {
305
385
  // NO_CODE_CHANGE / GitLab patch-id semantics: approvals are re-derivable from
306
386
  // substance-only digests, not carried). Each rule mirrors exactly one
307
387
  // lifecycle writer, scoped to the location that writer owns:
308
- // - frontmatter `status:` value (updateFrontmatterStatus, blueprint mutations)
309
- // - frontmatter `completed_at:` line (upsertCompletedAt)
388
+ // - frontmatter status/progress/owner fields (blueprint lifecycle mutations)
389
+ // - frontmatter `completed_at:` line (upsertCompletedAt)
310
390
  // - Readiness Verdict `- verified-at:`/`- verified-head:` items (upsertReadinessValue)
311
391
  // - Promotion Gates "Last result" cells (updateGateLastResult)
312
392
  // Any other byte change — task text, acceptance criteria, scope, lookalike
313
393
  // lines outside the owned locations — still changes the digest and dismisses
314
394
  // prior plan approvals.
395
+ const PLAN_TASK_STATUS_PLACEHOLDER = "__wp-task-status__";
396
+ // Neutralize the task-execution state the lifecycle engine writes
397
+ // (updateTaskStatus, applyVerification): `**Status:** <status>` lines,
398
+ // acceptance checkbox marks, and `**Verification:**` +
399
+ // ```webpresso-evidence-v1 fenced evidence blocks. Task execution is lifecycle
400
+ // state, not plan substance — without this, the engine's own task_verify /
401
+ // finalize writes dismiss the plan approval and no completion commit can ever
402
+ // carry authority. Acceptance-criteria TEXT stays substance; only the check
403
+ // mark is neutralized.
404
+ function neutralizeTaskExecutionState(text) {
405
+ const lines = text.split("\n");
406
+ const out = [];
407
+ for (let index = 0; index < lines.length; index += 1) {
408
+ const line = lines[index];
409
+ if (/^\*\*Verification:\*\*\s*$/u.test(line)) {
410
+ // Skip the writer-owned block: "**Verification:**", blank(s), the
411
+ // evidence fence, and one trailing blank line.
412
+ let cursor = index + 1;
413
+ while (cursor < lines.length && lines[cursor].trim() === "")
414
+ cursor += 1;
415
+ if (/^```webpresso-evidence-v1\s*$/u.test(lines[cursor] ?? "")) {
416
+ cursor += 1;
417
+ while (cursor < lines.length && !/^```\s*$/u.test(lines[cursor]))
418
+ cursor += 1;
419
+ cursor += 1; // closing fence
420
+ if ((lines[cursor] ?? "").trim() === "")
421
+ cursor += 1;
422
+ index = cursor - 1;
423
+ continue;
424
+ }
425
+ }
426
+ if (/^\*\*Status:\*\* (?:todo|in-progress|blocked|done|dropped)\s*$/u.test(line)) {
427
+ out.push(`**Status:** ${PLAN_TASK_STATUS_PLACEHOLDER}`);
428
+ continue;
429
+ }
430
+ out.push(line.replace(/^(\s*)- \[[xX ]\] /u, "$1- [_] "));
431
+ }
432
+ return out.join("\n");
433
+ }
315
434
  export function normalizePlanSubjectContent(content) {
316
435
  let text = typeof content === "string" ? content : content.toString("utf8");
317
436
  text = neutralizeLifecycleFrontmatter(text);
318
437
  text = neutralizeReadinessStamps(text);
319
438
  text = neutralizePromotionGateResults(text);
439
+ text = neutralizeTaskExecutionState(text);
320
440
  return Buffer.from(text, "utf8");
321
441
  }
322
- export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
442
+ /**
443
+ * Plan-subject digest for in-memory (working-tree) blueprint content. Used to
444
+ * accept working-ledger review events that digest-bind the exact bytes being
445
+ * committed (pre-commit runs before those events reach any git ref).
446
+ */
447
+ export function createPlanSubjectDigestForContent(blueprintSlug, content) {
448
+ const hash = createHash("sha256").update(PLAN_DOMAIN);
449
+ updateField(hash, blueprintSlug);
450
+ updateField(hash, normalizePlanSubjectContent(content));
451
+ return hash.digest("hex");
452
+ }
453
+ /**
454
+ * Digest-compatibility variant for review events recorded before the
455
+ * stripped-key extension of `neutralizeLifecycleFrontmatter`. Verification
456
+ * accepts a match against either normalization; new digests always use
457
+ * `normalizePlanSubjectContent`.
458
+ */
459
+ export function legacyNormalizePlanSubjectContent(content) {
460
+ let text = typeof content === "string" ? content : content.toString("utf8");
461
+ text = legacyNeutralizeLifecycleFrontmatter(text);
462
+ text = neutralizeReadinessStamps(text);
463
+ text = neutralizePromotionGateResults(text);
464
+ return Buffer.from(text, "utf8");
465
+ }
466
+ function planSubjectAtRef(cwd, ref, blueprintSlug, normalize, blueprintPath) {
323
467
  const commit = resolveReviewCommit(cwd, ref);
324
468
  const entries = parseTree(cwd, commit);
325
- const blueprintRoot = findBlueprintRoot(entries, blueprintSlug, blueprintPath);
326
- const overviewPath = Buffer.concat([blueprintRoot, Buffer.from("_overview.md", "ascii")]);
327
- const overview = entries.find((entry) => entry.path.equals(overviewPath));
469
+ const overview = findBlueprintDocument(entries, blueprintSlug, blueprintPath);
470
+ const overviewPath = overview.path;
328
471
  if (!overview || overview.type !== "blob") {
329
472
  throw new Error("Tracked blueprint overview is not a blob.");
330
473
  }
@@ -333,10 +476,7 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
333
476
  throw new Error("Tracked blueprint overview content is unavailable.");
334
477
  const hash = createHash("sha256").update(PLAN_DOMAIN);
335
478
  updateField(hash, blueprintSlug);
336
- // Hash normalized content. This redefines wp-blueprint-v1 in place, which is
337
- // safe pre-merge: no committed review-events.jsonl exists anywhere, so no
338
- // recorded plan digest can break.
339
- updateField(hash, normalizePlanSubjectContent(content));
479
+ updateField(hash, normalize(content));
340
480
  return {
341
481
  scheme: "wp-blueprint-v1",
342
482
  digest: hash.digest("hex"),
@@ -344,6 +484,13 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
344
484
  files: [overviewPath.toString("utf8")],
345
485
  };
346
486
  }
487
+ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
488
+ return planSubjectAtRef(cwd, ref, blueprintSlug, normalizePlanSubjectContent, blueprintPath);
489
+ }
490
+ /** Plan subject digested with the pre-extension normalization (see above). */
491
+ export function createLegacyPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
492
+ return planSubjectAtRef(cwd, ref, blueprintSlug, legacyNormalizePlanSubjectContent, blueprintPath);
493
+ }
347
494
  export const reviewSubjectInternals = {
348
495
  isExcludedReviewEnvelope,
349
496
  isReviewEvidencePath,
@@ -14,6 +14,7 @@ export interface ResolveRuntimeEnvironmentOptions {
14
14
  }
15
15
  export interface RuntimeSpawnOptions {
16
16
  readonly cwd?: string;
17
+ readonly envCwd?: string;
17
18
  readonly profile?: RuntimeSelector;
18
19
  readonly secretProfile?: string;
19
20
  readonly environment?: string;
@@ -82,9 +82,10 @@ export function resolveRuntimeEnvironment(options = {}) {
82
82
  }
83
83
  export function buildRuntimeSpawnOptions(options = {}) {
84
84
  const cwd = options.cwd ?? process.cwd();
85
+ const envCwd = options.envCwd ?? cwd;
85
86
  const cache = options.cache ?? createRuntimeEnvCache();
86
87
  const resolvedEnv = resolveRuntimeEnvironment({
87
- cwd,
88
+ cwd: envCwd,
88
89
  profile: options.profile,
89
90
  secretProfile: options.secretProfile,
90
91
  environment: options.environment,
@@ -0,0 +1,34 @@
1
+ import { type OpencodeAccount } from "#review/opencode-account.js";
2
+ import { resolveRuntimeEnvironment } from "./executor.js";
3
+ export type OpencodeAccountsResolution = {
4
+ readonly ok: true;
5
+ readonly accounts: readonly OpencodeAccount[];
6
+ } | {
7
+ readonly ok: false;
8
+ readonly reason: string;
9
+ };
10
+ export interface ResolveOpencodeAccountsGracefullyInput {
11
+ readonly cwd: string;
12
+ /**
13
+ * Secret profile to resolve through — REQUIRED, never optional.
14
+ *
15
+ * Without it `resolveRuntimeEnvironment` passes no environment selector and
16
+ * the secret manager falls back to its own ambient, path-keyed config
17
+ * (Doppler's per-directory scopes, Infisical's discovered `.infisical.json`).
18
+ * That resolves only in a checkout that happens to sit under the scoped path,
19
+ * so discovery silently yields zero accounts in every managed worktree, CI
20
+ * clone, and fresh checkout — a failure mode that is invisible because callers
21
+ * degrade gracefully. Keeping this required makes that class of bug a compile
22
+ * error instead of a silent runtime degradation. Callers pass
23
+ * `resolveAgentSecretProfile(env)`.
24
+ */
25
+ readonly secretProfile: string;
26
+ readonly resolveRuntimeEnvironment?: typeof resolveRuntimeEnvironment;
27
+ }
28
+ /**
29
+ * Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
30
+ * On any resolution failure (missing profile, secret-manager error, empty
31
+ * result), returns a failure reason derived only from the error's own
32
+ * message/type — never from a secret value.
33
+ */
34
+ export declare function resolveOpencodeAccountsGracefully(input: ResolveOpencodeAccountsGracefullyInput): OpencodeAccountsResolution;
@@ -0,0 +1,33 @@
1
+ import { resolveOpencodeAccounts } from "#review/opencode-account.js";
2
+ import { resolveRuntimeEnvironment } from "./executor.js";
3
+ function describeError(error) {
4
+ if (error instanceof Error)
5
+ return `${error.name}: ${error.message}`;
6
+ return typeof error === "string" ? error : "unknown error";
7
+ }
8
+ /**
9
+ * Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
10
+ * On any resolution failure (missing profile, secret-manager error, empty
11
+ * result), returns a failure reason derived only from the error's own
12
+ * message/type — never from a secret value.
13
+ */
14
+ export function resolveOpencodeAccountsGracefully(input) {
15
+ const resolveEnv = input.resolveRuntimeEnvironment ?? resolveRuntimeEnvironment;
16
+ let secrets;
17
+ try {
18
+ secrets = resolveEnv({
19
+ cwd: input.cwd,
20
+ profile: "secrets-only",
21
+ secretProfile: input.secretProfile,
22
+ environment: input.secretProfile,
23
+ });
24
+ }
25
+ catch (error) {
26
+ return { ok: false, reason: describeError(error) };
27
+ }
28
+ const accounts = resolveOpencodeAccounts(secrets);
29
+ if (accounts.length === 0) {
30
+ return { ok: false, reason: "no OPENCODE_GO_KEY_* secrets found" };
31
+ }
32
+ return { ok: true, accounts };
33
+ }
@@ -11,6 +11,7 @@ export interface SecretGateCommandOptions {
11
11
  readonly command: string;
12
12
  readonly args?: readonly string[];
13
13
  readonly cwd?: string;
14
+ readonly commandCwd?: string;
14
15
  readonly timeoutMs?: number;
15
16
  readonly signal?: AbortSignal;
16
17
  readonly forceSecretGate?: boolean;
@@ -35,6 +35,7 @@ export function buildSecretGateCommand(options) {
35
35
  sink,
36
36
  "--profile",
37
37
  profile,
38
+ ...(options.commandCwd ? ["--command-cwd", options.commandCwd] : []),
38
39
  "--",
39
40
  options.command,
40
41
  ...(options.args ?? []),
@@ -45,9 +46,11 @@ export function runSecretGateCommand(options) {
45
46
  const timeoutMs = options.timeoutMs ?? 30_000;
46
47
  const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
47
48
  const command = buildSecretGateCommand(options);
49
+ const runsInnerCommandDirectly = command.command === options.command;
50
+ const spawnCwd = runsInnerCommandDirectly && options.commandCwd ? options.commandCwd : options.cwd;
48
51
  return new Promise((resolve) => {
49
52
  const child = spawn(command.command, [...command.args], {
50
- cwd: options.cwd,
53
+ cwd: spawnCwd,
51
54
  env: buildRuntimeProcessEnv(options.cwd, process.env),
52
55
  detached: process.platform !== "win32",
53
56
  });
@@ -1,11 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { z } from "zod";
3
3
  import { evaluateApprovalReviewers } from "#lifecycle/audit.js";
4
+ import { collectV2ReviewProvenance } from "#lifecycle/review-provenance.js";
4
5
  import { evaluateRequiredChecks } from "#mcp/tools/_shared/required-checks.js";
5
6
  import { redactText } from "#mcp/tools/_shared/redact.js";
6
7
  import { runCommand } from "#mcp/tools/_shared/run-command.js";
7
8
  import { readReviewAuthorityAtRef } from "#review/authority.js";
8
9
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
10
+ import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
9
11
  export const statusLevelSchema = z.enum(["ready", "blocked", "partial", "unknown"]);
10
12
  export const requiredCheckEvaluationSchema = z.object({
11
13
  total: z.number().int().nonnegative(),
@@ -80,6 +82,11 @@ export const wpStatusSnapshotSchema = z.object({
80
82
  });
81
83
  const DEFAULT_TIMEOUT_MS = 30_000;
82
84
  const DEFAULT_MAX_OUTPUT_BYTES = 64_000;
85
+ // `gh pr view --json` includes the PR `body`, which alone can exceed the small
86
+ // caller diagnostic budget (GitHub bodies run to tens of KB). Parse that JSON
87
+ // against a generous semantic budget so mergeability/checks/blueprint-linkage
88
+ // fields are never lost to truncation; diagnostics keep the small caller budget.
89
+ const PR_VIEW_SEMANTIC_MAX_OUTPUT_BYTES = 1_048_576;
83
90
  const DISCLOSURE_FIELDS = [
84
91
  "Execution model(s)",
85
92
  "Planning/refinement model(s)",
@@ -115,8 +122,8 @@ async function probe(cwd, command, args, options = {}) {
115
122
  cwd,
116
123
  timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
117
124
  });
125
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
118
126
  if ("error" in outcome) {
119
- const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
120
127
  return {
121
128
  passed: false,
122
129
  semanticStdout: "",
@@ -125,11 +132,11 @@ async function probe(cwd, command, args, options = {}) {
125
132
  missingBinary: outcome.error.code === "ENOENT",
126
133
  };
127
134
  }
128
- const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
135
+ const semanticMaxOutputBytes = options.semanticMaxOutputBytes ?? maxOutputBytes;
129
136
  return {
130
137
  passed: outcome.exitCode === 0,
131
- semanticStdout: clipBytes(outcome.stdout, maxOutputBytes),
132
- semanticTruncated: Buffer.byteLength(outcome.stdout, "utf8") > maxOutputBytes,
138
+ semanticStdout: clipBytes(outcome.stdout, semanticMaxOutputBytes),
139
+ semanticTruncated: Buffer.byteLength(outcome.stdout, "utf8") > semanticMaxOutputBytes,
133
140
  stdout: clipDiagnostic(outcome.stdout, maxOutputBytes),
134
141
  stderr: clipDiagnostic(outcome.stderr, maxOutputBytes),
135
142
  exitCode: outcome.exitCode,
@@ -307,33 +314,29 @@ function selectBlueprintFile(projectRoot, pr) {
307
314
  const prefix = `${root}/`;
308
315
  if (!file.path.startsWith(prefix))
309
316
  continue;
310
- const segments = file.path.slice(prefix.length).split("/");
311
- if (segments.length < 3 ||
312
- segments.at(-1) !== "_overview.md" ||
313
- segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
314
- continue;
315
- }
316
- const stableSlug = segments.slice(1, -1).join("/");
317
- if (stableSlug)
318
- return [{ ...file, stableSlug }];
317
+ const parsed = parseBlueprintDocumentRelativePath(file.path.slice(prefix.length));
318
+ if (parsed)
319
+ return [{ ...file, stableSlug: parsed.slug }];
319
320
  }
320
321
  return [];
321
322
  });
322
323
  const priority = (file) => file.changeType === "ADDED" ? 0 : file.changeType === "RENAMED" ? 1 : 2;
323
324
  return candidates.toSorted((left, right) => priority(left) - priority(right))[0];
324
325
  }
325
- function resolveBlueprintEvidence(cwd, projectRoot, pr, targetHash) {
326
+ function resolveBlueprintEvidence(projectRoot, pr, targetHash) {
326
327
  const blueprintFile = selectBlueprintFile(projectRoot, pr);
327
328
  if (!blueprintFile || !targetHash)
328
329
  return undefined;
329
330
  try {
330
- const authority = readReviewAuthorityAtRef(cwd, targetHash, blueprintFile.stableSlug, "delivery");
331
+ const authority = readReviewAuthorityAtRef(projectRoot, targetHash, blueprintFile.stableSlug, "delivery");
331
332
  if (authority.blueprintPath !== blueprintFile.path)
332
333
  return undefined;
334
+ const approvals = collectV2ReviewProvenance(path.resolve(projectRoot, blueprintFile.path), "delivery", targetHash);
333
335
  return {
334
336
  path: blueprintFile.path,
335
337
  lifecycle: authority.lifecycle,
336
- approvalsSatisfied: evaluateApprovalReviewers(authority.approvals.map((approval) => approval.reviewer)).satisfied,
338
+ approvalsSatisfied: evaluateApprovalReviewers(approvals.map((approval) => approval.reviewer))
339
+ .satisfied,
337
340
  };
338
341
  }
339
342
  catch {
@@ -582,7 +585,10 @@ export async function buildStatusSnapshot(options = {}) {
582
585
  "files",
583
586
  ].join(",");
584
587
  const [view, checksProbe] = await Promise.all([
585
- probe(cwd, "gh", ["pr", "view", ...selector, "--json", fields], probeOptions),
588
+ probe(cwd, "gh", ["pr", "view", ...selector, "--json", fields], {
589
+ ...probeOptions,
590
+ semanticMaxOutputBytes: PR_VIEW_SEMANTIC_MAX_OUTPUT_BYTES,
591
+ }),
586
592
  probe(cwd, "gh", ["pr", "checks", ...selector, "--required", "--json", "name,state,bucket,link,description"], probeOptions),
587
593
  ]);
588
594
  pr = view.passed ? parseJsonObject(view.semanticStdout) : undefined;
@@ -599,7 +605,7 @@ export async function buildStatusSnapshot(options = {}) {
599
605
  readGreptileComments(cwd, pr, probeOptions),
600
606
  ])
601
607
  : [{ complete: false, warning: "PR metadata unavailable." }, { comments: [] }];
602
- const blueprint = resolveBlueprintEvidence(cwd, firstLine(topLevel.semanticStdout) ?? cwd, pr, localHead);
608
+ const blueprint = resolveBlueprintEvidence(firstLine(topLevel.semanticStdout) ?? cwd, pr, localHead);
603
609
  const readiness = evaluatePrReadiness({
604
610
  localHead,
605
611
  dirtyWorktree,
@@ -649,9 +655,14 @@ export async function buildStatusSnapshot(options = {}) {
649
655
  threadsComplete: threads.complete,
650
656
  greptile: classifyGreptile(checks ?? [], greptileComments.comments, stringField(pr?.headRefOid)),
651
657
  };
658
+ // `body` is fully consumed above (release-evidence + disclosure via
659
+ // `evaluatePrReadiness`, blueprint linkage via `resolveBlueprintEvidence`),
660
+ // so drop it from the emitted metadata to keep the payload from re-ballooning
661
+ // to the size that truncated the parse in the first place.
662
+ const { body: _body, ...metadataWithoutBody } = pr ?? {};
652
663
  context.pr = {
653
664
  selector: selector[0],
654
- metadata: pr,
665
+ metadata: pr ? metadataWithoutBody : undefined,
655
666
  checks: readiness.checks
656
667
  ? { ...readiness.checks, checks: [...readiness.checks.checks] }
657
668
  : undefined,
@@ -0,0 +1,20 @@
1
+ import { BaseSequencer, type TestSpecification } from "vitest/node";
2
+ export declare function weightFor(project: string, relativePath: string): number;
3
+ interface WeightedSpec {
4
+ readonly spec: TestSpecification;
5
+ readonly weight: number;
6
+ readonly key: string;
7
+ }
8
+ /**
9
+ * Deterministic greedy LPT partition of `specs` into `count` bins. Files are
10
+ * sorted heaviest-first (ties broken by path so the order is stable across
11
+ * every shard job), then each is placed into the currently-lightest bin (ties
12
+ * broken by lowest bin index). Returns all bins in index order.
13
+ *
14
+ * Exported for the contract test; the sequencer only needs one bin.
15
+ */
16
+ export declare function partitionByDuration(specs: readonly WeightedSpec[], count: number): readonly (readonly WeightedSpec[])[];
17
+ export declare class DurationSequencer extends BaseSequencer {
18
+ shard(files: TestSpecification[]): Promise<TestSpecification[]>;
19
+ }
20
+ export {};
@@ -0,0 +1,54 @@
1
+ import { relative } from "node:path";
2
+ import { BaseSequencer } from "vitest/node";
3
+ import rawSnapshot from "./shard-durations.json" with { type: "json" };
4
+ const snapshot = rawSnapshot;
5
+ // A generated snapshot always carries a positive defaultWeight; clamp to 1 only
6
+ // to keep LPT well-defined if the JSON is ever hand-truncated to nothing.
7
+ const DEFAULT_WEIGHT = snapshot.defaultWeight > 0 ? snapshot.defaultWeight : 1;
8
+ export function weightFor(project, relativePath) {
9
+ return snapshot.durations[project]?.[relativePath] ?? DEFAULT_WEIGHT;
10
+ }
11
+ /**
12
+ * Deterministic greedy LPT partition of `specs` into `count` bins. Files are
13
+ * sorted heaviest-first (ties broken by path so the order is stable across
14
+ * every shard job), then each is placed into the currently-lightest bin (ties
15
+ * broken by lowest bin index). Returns all bins in index order.
16
+ *
17
+ * Exported for the contract test; the sequencer only needs one bin.
18
+ */
19
+ export function partitionByDuration(specs, count) {
20
+ const bins = Array.from({ length: count }, () => []);
21
+ const loads = Array.from({ length: count }, () => 0);
22
+ const ordered = [...specs].sort((a, b) => b.weight !== a.weight ? b.weight - a.weight : a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
23
+ for (const item of ordered) {
24
+ let target = 0;
25
+ let targetLoad = loads[0] ?? 0;
26
+ for (let i = 1; i < count; i += 1) {
27
+ const load = loads[i] ?? 0;
28
+ if (load < targetLoad) {
29
+ target = i;
30
+ targetLoad = load;
31
+ }
32
+ }
33
+ // target is always in [0, count): bins/loads were sized to `count`.
34
+ (bins[target] ??= []).push(item);
35
+ loads[target] = targetLoad + item.weight;
36
+ }
37
+ return bins;
38
+ }
39
+ export class DurationSequencer extends BaseSequencer {
40
+ async shard(files) {
41
+ const { config } = this.ctx;
42
+ // No --shard (serial lane, local full runs): nothing to partition.
43
+ if (!config.shard)
44
+ return files;
45
+ const { index, count } = config.shard;
46
+ const weighted = files.map((spec) => {
47
+ const key = relative(config.root, spec.moduleId);
48
+ return { spec, key, weight: weightFor(spec.project.name, key) };
49
+ });
50
+ const bins = partitionByDuration(weighted, count);
51
+ // index is 1-based in [1, count]; the bin is always present.
52
+ return (bins[index - 1] ?? []).map((item) => item.spec);
53
+ }
54
+ }