@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.

Potentially problematic release.


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

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
@@ -7,4 +7,10 @@ export interface VitestWorkerBudget {
7
7
  readonly subprocess: number;
8
8
  }
9
9
  export declare function parsePositiveInteger(value: string | undefined): number | undefined;
10
+ /**
11
+ * Allocate one worker ceiling across concurrently running Vitest projects.
12
+ * Standalone project runs keep `computeVitestWorkerBudget`; only the supervisor
13
+ * uses this aggregate contract.
14
+ */
15
+ export declare function computeConcurrentVitestWorkerBudget(options?: VitestWorkerBudgetOptions): VitestWorkerBudget;
10
16
  export declare function computeVitestWorkerBudget(options?: VitestWorkerBudgetOptions): VitestWorkerBudget;
@@ -9,12 +9,70 @@ function splitSharedBudget(totalWorkers) {
9
9
  if (totalWorkers <= 2) {
10
10
  return { unit: 1, subprocess: 1 };
11
11
  }
12
- const unit = Math.max(1, Math.round(totalWorkers * (2 / 3)));
12
+ // Even split. The lanes carry near-equal total work (measured 2026-07-23 on
13
+ // an 8-core host: unit ≈320 worker-seconds, subprocess ≈266 — see
14
+ // docs/ci-test-perf.md), so the previous 2/3-unit skew starved the
15
+ // subprocess lane: its makespan, not unit's, bounded the whole run
16
+ // (subprocess lane >296s at 2 workers vs 168s at 4 on the same host).
17
+ // CI is unaffected — it pins WP_VITEST_MAX_WORKERS=2, taking the early
18
+ // return above.
19
+ const unit = Math.max(1, Math.round(totalWorkers / 2));
13
20
  return {
14
21
  unit,
15
22
  subprocess: Math.max(1, totalWorkers - unit),
16
23
  };
17
24
  }
25
+ function readWorkerOverride(env, name) {
26
+ const raw = env[name];
27
+ if (raw === undefined)
28
+ return;
29
+ const parsed = parsePositiveInteger(raw);
30
+ if (parsed === undefined) {
31
+ throw new Error(`${name} must be a positive integer, got ${JSON.stringify(raw)}`);
32
+ }
33
+ return parsed;
34
+ }
35
+ /**
36
+ * Allocate one worker ceiling across concurrently running Vitest projects.
37
+ * Standalone project runs keep `computeVitestWorkerBudget`; only the supervisor
38
+ * uses this aggregate contract.
39
+ */
40
+ export function computeConcurrentVitestWorkerBudget(options = {}) {
41
+ const env = options.env ?? process.env;
42
+ const sharedOverride = readWorkerOverride(env, "WP_VITEST_MAX_WORKERS");
43
+ const unitOverride = readWorkerOverride(env, "WP_VITEST_UNIT_MAX_WORKERS");
44
+ const subprocessOverride = readWorkerOverride(env, "WP_VITEST_SUBPROCESS_MAX_WORKERS");
45
+ if (sharedOverride === undefined &&
46
+ unitOverride !== undefined &&
47
+ subprocessOverride !== undefined) {
48
+ return { unit: unitOverride, subprocess: subprocessOverride };
49
+ }
50
+ const total = sharedOverride ?? (env.CI === "true" ? 2 : defaultSharedWorkers(options));
51
+ if (total < 2) {
52
+ throw new Error(`WP_VITEST_MAX_WORKERS must be at least 2 for concurrent projects, got ${total}`);
53
+ }
54
+ if (unitOverride !== undefined && subprocessOverride !== undefined) {
55
+ if (unitOverride + subprocessOverride > total) {
56
+ throw new Error(`Lane worker overrides (${unitOverride + subprocessOverride}) exceed aggregate worker budget ${total}`);
57
+ }
58
+ return { unit: unitOverride, subprocess: subprocessOverride };
59
+ }
60
+ if (unitOverride !== undefined) {
61
+ const subprocess = total - unitOverride;
62
+ if (subprocess < 1) {
63
+ throw new Error(`Unit worker override ${unitOverride} leaves no aggregate subprocess worker`);
64
+ }
65
+ return { unit: unitOverride, subprocess };
66
+ }
67
+ if (subprocessOverride !== undefined) {
68
+ const unit = total - subprocessOverride;
69
+ if (unit < 1) {
70
+ throw new Error(`Subprocess worker override ${subprocessOverride} leaves no aggregate unit worker`);
71
+ }
72
+ return { unit, subprocess: subprocessOverride };
73
+ }
74
+ return splitSharedBudget(total);
75
+ }
18
76
  export function computeVitestWorkerBudget(options = {}) {
19
77
  const env = options.env ?? process.env;
20
78
  const sharedOverride = parsePositiveInteger(env.WP_VITEST_MAX_WORKERS);
@@ -1,3 +1,2 @@
1
1
  import type { TestProject } from "vitest/node";
2
- export declare function shouldPrepareDistForProject(projectName: string | undefined): boolean;
3
- export declare function setup(project?: Pick<TestProject, "name">): void;
2
+ export declare function setup(project?: Pick<TestProject, "name">): Promise<void>;
@@ -1,44 +1,91 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import lockfile from "proper-lockfile";
4
5
  import { syncSastRuleAssets } from "#build/sast-rule-assets.js";
6
+ import { runFormat } from "#format/index.js";
5
7
  import { ensureHostRuntimeArtifactsForSerialTests } from "./runtime-binary-artifacts.js";
6
8
  const REPO_ROOT = process.cwd();
7
9
  const DIST_SENTINEL = join(REPO_ROOT, "dist", "esm", "index.js");
8
10
  const MIGRATION_SENTINEL = join(REPO_ROOT, "dist", "esm", "blueprint", "db", "migrations", "0001_seed.sql");
9
- const DIST_PREP_PROJECTS = new Set(["subprocess", "serial-subprocess"]);
10
- export function shouldPrepareDistForProject(projectName) {
11
- return projectName !== undefined && DIST_PREP_PROJECTS.has(projectName);
12
- }
13
- // Build before subprocess workers fork tshy's non-atomic package.json write races with bun subprocess reads in init.e2e.test.ts.
14
- // Unit-only runs should not pay this build/setup cost, and Vitest passes the selected TestProject to globalSetup.
15
- export function setup(project) {
16
- syncSastRuleAssets(REPO_ROOT);
17
- if (!shouldPrepareDistForProject(project?.name))
18
- return;
19
- let builtDist = false;
20
- if (!existsSync(DIST_SENTINEL)) {
21
- execFileSync("./node_modules/.bin/tshy", [], {
22
- cwd: REPO_ROOT,
23
- encoding: "utf8",
24
- env: { ...process.env, HUSKY: "0" },
25
- });
26
- builtDist = true;
27
- }
28
- if (!existsSync(MIGRATION_SENTINEL)) {
29
- execFileSync("bun", ["src/build/blueprint-migration-assets.ts"], {
30
- cwd: REPO_ROOT,
31
- encoding: "utf8",
32
- env: { ...process.env, HUSKY: "0" },
33
- });
11
+ // Lock target lives outside dist/ (tshy rewrites dist/) and under the always-present, gitignored
12
+ // node_modules/.cache so a concurrent test run cannot clobber it mid-build.
13
+ const BUILD_LOCK_TARGET = join(REPO_ROOT, "node_modules", ".cache", "wp-test-setup");
14
+ // Serialize the dist build across concurrent test runs sharing this checkout: tshy's non-atomic
15
+ // dist/package.json writes corrupt each other if two runs build at once. proper-lockfile gives a
16
+ // bounded wait plus native stale-owner recovery (a dead builder's lock is reclaimed after `stale`).
17
+ // The retry budget spans well past a cold build so a second run waits rather than failing closed.
18
+ async function withDistBuildLock(fn) {
19
+ mkdirSync(join(REPO_ROOT, "node_modules", ".cache"), { recursive: true });
20
+ const release = await lockfile.lock(BUILD_LOCK_TARGET, {
21
+ realpath: false,
22
+ stale: 120_000,
23
+ retries: { retries: 40, factor: 1.4, minTimeout: 200, maxTimeout: 5_000 },
24
+ });
25
+ try {
26
+ return await fn();
34
27
  }
35
- if (builtDist) {
36
- execFileSync("bun", ["src/build/normalize-tsconfig-json-exports.ts"], {
37
- cwd: REPO_ROOT,
38
- encoding: "utf8",
39
- env: { ...process.env, HUSKY: "0" },
40
- });
28
+ finally {
29
+ await release();
41
30
  }
31
+ }
32
+ // h-019: scripts/run-vitest-projects.ts spawns the "unit" and "subprocess" vitest projects as two
33
+ // independent, unsynchronized child processes (Promise.all) — the same exposure exists for any two
34
+ // `vitest run --project <name>` invocations that happen to share this checkout. tshy's package.json
35
+ // write below is non-atomic (truncate+write, not write-temp+rename): a project whose setup() returned
36
+ // without taking this lock could start reading package.json (module resolution, etc.) while a sibling
37
+ // project's process was mid-tshy-write, observing a truncated/empty file (JSONError "File is empty").
38
+ // Every project therefore passes through this SAME gate before its tests can start, whether or not
39
+ // that project needs dist output for itself: whichever process reaches the lock first performs the
40
+ // idempotent, re-checked-under-lock build; every other process blocks until that release, then finds
41
+ // the sentinels already satisfied and returns immediately. In the steady state (dist/ already built)
42
+ // this is a single existsSync check with no lock contention at all.
43
+ async function ensureDistPrepared() {
44
+ // Fast path: nothing to build. Only pay the lock when a build artifact is actually missing.
45
+ if (existsSync(DIST_SENTINEL) && existsSync(MIGRATION_SENTINEL))
46
+ return;
47
+ await withDistBuildLock(async () => {
48
+ let builtDist = false;
49
+ // Re-check under the lock: another run may have completed the build while we waited.
50
+ if (!existsSync(DIST_SENTINEL)) {
51
+ execFileSync("./node_modules/.bin/tshy", [], {
52
+ cwd: REPO_ROOT,
53
+ encoding: "utf8",
54
+ env: { ...process.env, HUSKY: "0" },
55
+ });
56
+ builtDist = true;
57
+ // tshy rewrites package.json's exports with a trailing newline the repo formatter forbids
58
+ // (`.editorconfig [*.json] insert_final_newline = false`), which would leave a clean worktree
59
+ // dirty. The real `build` script (package.json `"build"`) restores it with `wp format --file
60
+ // package.json`; mirror that here in-process (runFormat spawns only `vp fmt`, avoiding a second
61
+ // cold CLI launch) so `wp test` never dirties package.json. See package-scripts.test.ts.
62
+ // Fail loudly if the restore does not succeed — a silent failure would ship a dirty package.json.
63
+ const restored = await runFormat({ files: ["package.json"], cwd: REPO_ROOT });
64
+ if (!restored.passed) {
65
+ throw new Error(`global-setup: could not restore package.json formatting after tshy (exit ${restored.exitCode})` +
66
+ `${restored.spawnError ? `: ${restored.spawnError}` : ""}\n${restored.output}`);
67
+ }
68
+ }
69
+ if (!existsSync(MIGRATION_SENTINEL)) {
70
+ execFileSync("bun", ["src/build/blueprint-migration-assets.ts"], {
71
+ cwd: REPO_ROOT,
72
+ encoding: "utf8",
73
+ env: { ...process.env, HUSKY: "0" },
74
+ });
75
+ }
76
+ if (builtDist) {
77
+ execFileSync("bun", ["src/build/normalize-tsconfig-json-exports.ts"], {
78
+ cwd: REPO_ROOT,
79
+ encoding: "utf8",
80
+ env: { ...process.env, HUSKY: "0" },
81
+ });
82
+ }
83
+ });
84
+ }
85
+ // Vitest passes the selected TestProject to globalSetup, once per project process.
86
+ export async function setup(project) {
87
+ syncSastRuleAssets(REPO_ROOT);
88
+ await ensureDistPrepared();
42
89
  if (project?.name === "serial-subprocess") {
43
90
  ensureHostRuntimeArtifactsForSerialTests({ rootDir: REPO_ROOT });
44
91
  }
@@ -13,7 +13,7 @@ export interface PlannedTypecheckCommand {
13
13
  }
14
14
  export interface TypecheckExecutionPlan {
15
15
  readonly repoRoot: string;
16
- readonly mode: "default" | "files" | "packages";
16
+ readonly mode: "default" | "files" | "packages" | "tests";
17
17
  readonly commands: readonly PlannedTypecheckCommand[];
18
18
  readonly resolvedScopes: readonly TypecheckScope[];
19
19
  readonly preambleLine?: string;
@@ -24,6 +24,14 @@ export interface PlanTypecheckExecutionOptions {
24
24
  readonly files?: readonly string[];
25
25
  readonly packages?: readonly string[];
26
26
  readonly pretty?: boolean;
27
+ /**
28
+ * Run the separate test-surface typecheck lane (`tsconfig.test.json`)
29
+ * instead of the default production-source lane. Mutually exclusive with
30
+ * `files`/`packages`. This lane is invoked via `wp typecheck --tests` and is
31
+ * it is not part of the default `wp typecheck` gate or `ci-preflight`
32
+ * until the test-surface error corpus is burned down to zero.
33
+ */
34
+ readonly tests?: boolean;
27
35
  }
28
36
  export declare function planTypecheckExecution(options: PlanTypecheckExecutionOptions): TypecheckExecutionPlan;
29
37
  export declare function formatResolvedScopesLine(scopes: readonly TypecheckScope[]): string;
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { getPackageScript, readPackageJson, isRecursiveWpScript } from "#cli/package-scripts.js";
4
4
  import { getWorkspacePackages } from "#quality-engine/target-resolver.js";
5
5
  import { getManagedRunner, resolveEffectiveRunnerRoot } from "#tool-runtime";
6
+ const TEST_TSCONFIG_FILENAME = "tsconfig.test.json";
6
7
  const GENERATED_ROOT_PREFIXES = [".webpresso/generated"];
7
8
  const OUTPUT_SEGMENTS = new Set(["coverage", "dist", "node_modules"]);
8
9
  export function planTypecheckExecution(options) {
@@ -14,6 +15,17 @@ export function planTypecheckExecution(options) {
14
15
  if (files.length > 0 && packages.length > 0) {
15
16
  throw new Error("Cannot use both --file and --package for typecheck targeting.");
16
17
  }
18
+ if (options.tests) {
19
+ if (files.length > 0 || packages.length > 0) {
20
+ throw new Error("Cannot use --tests together with --file or --package.");
21
+ }
22
+ return {
23
+ repoRoot,
24
+ mode: "tests",
25
+ commands: [buildTestsCommand(defaultScopeRoot, options.pretty)],
26
+ resolvedScopes: [],
27
+ };
28
+ }
17
29
  if (packages.length > 0) {
18
30
  const scopes = resolvePackageScopes(repoRoot, realRepoRoot, packages);
19
31
  return {
@@ -63,6 +75,25 @@ function buildUntargetedCommand(cwd, pretty = false) {
63
75
  cwd,
64
76
  };
65
77
  }
78
+ function buildTestsCommand(cwd, pretty = false) {
79
+ const testTsconfigPath = join(cwd, TEST_TSCONFIG_FILENAME);
80
+ if (!existsSync(testTsconfigPath)) {
81
+ throw new Error(`--tests requires ${TEST_TSCONFIG_FILENAME} at the repo root (not found at ${testTsconfigPath}).`);
82
+ }
83
+ const resolution = getManagedRunner("tsc");
84
+ return {
85
+ scope: null,
86
+ command: resolution.command,
87
+ args: [
88
+ ...resolution.args,
89
+ "--noEmit",
90
+ "-p",
91
+ TEST_TSCONFIG_FILENAME,
92
+ ...(pretty ? [] : ["--pretty", "false"]),
93
+ ],
94
+ cwd,
95
+ };
96
+ }
66
97
  function buildTargetedCommand(scope, pretty = false) {
67
98
  const checkTypesScript = getPackageScript(scope.root, "check-types");
68
99
  if (checkTypesScript && !isRecursiveWpScript(checkTypesScript, "typecheck")) {
@@ -70,6 +70,13 @@ export declare function decideUltragoalRun(input: {
70
70
  readonly currentSourceSnapshot?: string;
71
71
  readonly now?: Date;
72
72
  }): UltragoalRunDecision;
73
+ /**
74
+ * Preflight every blueprint's document shape BEFORE a durable state
75
+ * transition. Ledger callbacks run after state is committed, so a flat
76
+ * blueprint must be rejected here or the command throws with state already
77
+ * written. Missing blueprints are tolerated exactly like the ledger append.
78
+ */
79
+ export declare function assertUltragoalBlueprintShapes(cwd: string, slugs: readonly string[]): void;
73
80
  export declare function createUltragoal(input: {
74
81
  readonly cwd?: string;
75
82
  readonly brief: string;
@@ -7,6 +7,7 @@ import { BlueprintCreationService } from "#service/BlueprintCreationService.js";
7
7
  import { BlueprintService } from "#service/BlueprintService.js";
8
8
  import { scanBlueprintDirectory } from "#service/scanner.js";
9
9
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
10
+ import { assertFolderBlueprintIdentity, resolveBlueprintDocumentIdentity, } from "#utils/document-paths.js";
10
11
  import { assertUltragoalEnabled, assertUltragoalMutationAllowed } from "./guards.js";
11
12
  import { evaluateUltragoalCompletion, hasRepeatedQaError, MAX_STOP_STREAK, MAX_VALIDATION_ROUNDS, } from "./completion.js";
12
13
  import { formatUltragoalHandoff } from "./handoff.js";
@@ -222,14 +223,43 @@ export function decideUltragoalRun(input) {
222
223
  }
223
224
  }
224
225
  function findBlueprint(cwd, slug) {
226
+ const baseDir = resolveBlueprintRoot(cwd);
225
227
  const entries = scanBlueprintDirectory({
226
- baseDir: resolveBlueprintRoot(cwd),
228
+ baseDir,
227
229
  includeSpecialFolders: true,
228
230
  });
229
231
  const found = entries.find((entry) => entry.slug === slug || entry.slug.endsWith(`/${slug}`));
230
232
  if (!found)
231
233
  throw new Error(`Blueprint not found: ${slug}`);
232
- return found;
234
+ const identity = resolveBlueprintDocumentIdentity(baseDir, found.path);
235
+ if (!identity) {
236
+ throw new Error(`Blueprint ${found.slug} resolved to an invalid document path: ${found.path}`);
237
+ }
238
+ return { path: found.path, shape: identity.shape, slug: found.slug };
239
+ }
240
+ /**
241
+ * Preflight every blueprint's document shape BEFORE a durable state
242
+ * transition. Ledger callbacks run after state is committed, so a flat
243
+ * blueprint must be rejected here or the command throws with state already
244
+ * written. Missing blueprints are tolerated exactly like the ledger append.
245
+ */
246
+ export function assertUltragoalBlueprintShapes(cwd, slugs) {
247
+ for (const slug of slugs) {
248
+ let found;
249
+ try {
250
+ found = findBlueprint(cwd, slug);
251
+ }
252
+ catch (error) {
253
+ if (error instanceof Error && error.message === `Blueprint not found: ${slug}`)
254
+ continue;
255
+ throw error;
256
+ }
257
+ assertFolderBlueprintIdentity({
258
+ documentPath: found.path,
259
+ lifecycleSlug: found.slug,
260
+ shape: found.shape,
261
+ });
262
+ }
233
263
  }
234
264
  function appendUltragoalLedgerEvent(cwd, slug, state, event) {
235
265
  let found;
@@ -241,6 +271,11 @@ function appendUltragoalLedgerEvent(cwd, slug, state, event) {
241
271
  return;
242
272
  throw error;
243
273
  }
274
+ assertFolderBlueprintIdentity({
275
+ documentPath: found.path,
276
+ lifecycleSlug: found.slug,
277
+ shape: found.shape,
278
+ });
244
279
  const reviewsPath = join(dirname(found.path), "reviews.md");
245
280
  mkdirSync(dirname(reviewsPath), { recursive: true });
246
281
  const prefix = existsSync(reviewsPath) ? "\n" : "# Blueprint reviews\n\n";
@@ -281,6 +316,7 @@ export async function createUltragoal(input) {
281
316
  sessionId: input.sessionId ?? process.env.WP_SESSION_ID ?? randomUUID(),
282
317
  ultragoalId: input.ultragoalId ?? `ultragoal-${randomUUID()}`,
283
318
  });
319
+ assertUltragoalBlueprintShapes(cwd, blueprintSlugs);
284
320
  await initializeUltragoalState(state, cwd, (written) => {
285
321
  for (const slug of blueprintSlugs)
286
322
  appendUltragoalLedgerEvent(cwd, slug, written, "created");
@@ -337,6 +373,9 @@ export async function getUltragoalHandoff(cwd = process.cwd()) {
337
373
  }
338
374
  export async function cancelUltragoal(cwd = process.cwd()) {
339
375
  assertUltragoalMutationAllowed(cwd);
376
+ const current = await readUltragoalState(cwd);
377
+ if (current)
378
+ assertUltragoalBlueprintShapes(cwd, current.blueprintSlugs);
340
379
  const next = await updateUltragoalState((state) => transitionUltragoalState(state, "cancelled"), cwd, (written) => {
341
380
  for (const slug of written.blueprintSlugs) {
342
381
  appendUltragoalLedgerEvent(cwd, slug, written, "cancelled");
@@ -394,6 +433,7 @@ export async function runCurrentUltragoal(cwd = process.cwd(), deps = {}) {
394
433
  now: deps.now?.() ?? new Date(),
395
434
  });
396
435
  if (committedDecision.state !== status.state) {
436
+ assertUltragoalBlueprintShapes(cwd, committedDecision.state.blueprintSlugs);
397
437
  await updateUltragoalState((current) => {
398
438
  if (current.ultragoalId !== status.state?.ultragoalId ||
399
439
  current.updatedAt !== status.state.updatedAt ||
@@ -1,4 +1,4 @@
1
- import { closeSync, copyFileSync, existsSync, fdatasyncSync, fsyncSync, openSync, renameSync, rmSync, writeFileSync, } from "node:fs";
1
+ import { closeSync, copyFileSync, existsSync, fdatasyncSync, fchmodSync, fsyncSync, openSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { dirname } from "node:path";
4
4
  export function writeJsonFile(path, data, options = {}) {
@@ -45,6 +45,8 @@ function writeFileAtomicWithPolicy(path, content, options, allowCrossDeviceFallb
45
45
  let installed = false;
46
46
  try {
47
47
  fd = openSync(tmpPath, "w", normalized.mode);
48
+ if (normalized.mode !== undefined)
49
+ fchmodSync(fd, normalized.mode);
48
50
  writeFileSync(fd, content, normalized.writeOptions);
49
51
  fdatasyncSync(fd);
50
52
  closeSync(fd);
@@ -0,0 +1,15 @@
1
+ export interface RemoveCodexProjectTrustOptions {
2
+ readonly configFilePath?: string;
3
+ }
4
+ /**
5
+ * Remove the exact Codex project-trust table for a worktree that has just been
6
+ * removed. This deliberately does not touch nested hook state or other project
7
+ * entries, which may remain valid user-owned configuration.
8
+ */
9
+ export declare function removeCodexProjectTrust(worktreePath: string, options?: RemoveCodexProjectTrustOptions): Promise<"removed" | "unchanged">;
10
+ /**
11
+ * Remove exact Codex project-trust tables in one locked rewrite. Prune can
12
+ * reconcile several externally deleted worktrees at once, so batching avoids
13
+ * taking the global config lock and rewriting the file for every entry.
14
+ */
15
+ export declare function removeCodexProjectTrusts(worktreePaths: readonly string[], options?: RemoveCodexProjectTrustOptions): Promise<"removed" | "unchanged">;
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import lockfile from "proper-lockfile";
3
+ import { defaultCodexConfigFilePath } from "#cli/commands/init/scaffolders/agent-hooks/codex-trust-sync.js";
4
+ import { writeFileAtomic } from "#shared-utils/write-json-file.js";
5
+ /**
6
+ * Remove the exact Codex project-trust table for a worktree that has just been
7
+ * removed. This deliberately does not touch nested hook state or other project
8
+ * entries, which may remain valid user-owned configuration.
9
+ */
10
+ export function removeCodexProjectTrust(worktreePath, options = {}) {
11
+ return removeCodexProjectTrusts([worktreePath], options);
12
+ }
13
+ /**
14
+ * Remove exact Codex project-trust tables in one locked rewrite. Prune can
15
+ * reconcile several externally deleted worktrees at once, so batching avoids
16
+ * taking the global config lock and rewriting the file for every entry.
17
+ */
18
+ export async function removeCodexProjectTrusts(worktreePaths, options = {}) {
19
+ if (worktreePaths.length === 0)
20
+ return "unchanged";
21
+ const configFilePath = options.configFilePath ?? defaultCodexConfigFilePath();
22
+ if (!existsSync(configFilePath))
23
+ return "unchanged";
24
+ const release = await lockfile.lock(configFilePath, {
25
+ realpath: false,
26
+ retries: { retries: 8, factor: 1.5, minTimeout: 25, maxTimeout: 250 },
27
+ });
28
+ try {
29
+ const existing = readFileSync(configFilePath, "utf8");
30
+ const next = removeProjectTrustTables(existing, worktreePaths);
31
+ if (next === existing)
32
+ return "unchanged";
33
+ writeFileAtomic(configFilePath, next, "utf8");
34
+ return "removed";
35
+ }
36
+ finally {
37
+ await release();
38
+ }
39
+ }
40
+ function removeProjectTrustTables(config, worktreePaths) {
41
+ const lines = config.split("\n");
42
+ const headers = new Set(worktreePaths.map((worktreePath) => `[projects.${JSON.stringify(worktreePath)}]`));
43
+ let removed = false;
44
+ for (let start = 0; start < lines.length;) {
45
+ if (!headers.has(lines[start]?.trim() ?? "")) {
46
+ start += 1;
47
+ continue;
48
+ }
49
+ let end = start + 1;
50
+ while (end < lines.length && !/^\s*\[.+\]\s*$/u.test(lines[end] ?? ""))
51
+ end += 1;
52
+ lines.splice(start, end - start);
53
+ removed = true;
54
+ }
55
+ return removed ? lines.join("\n") : config;
56
+ }
@@ -0,0 +1,50 @@
1
+ import { type WorktreeInventory } from "./identity.js";
2
+ export type OrphanClassification = "alive" | "orphan" | "broken" | "unrecognized";
3
+ export interface OrphanScanCandidate {
4
+ readonly path: string;
5
+ readonly repoNamespace: string;
6
+ }
7
+ export interface OrphanScanRow extends OrphanScanCandidate {
8
+ readonly classification: OrphanClassification;
9
+ readonly dirty: boolean | "unknown";
10
+ /**
11
+ * Recursive size, computed ONLY for the final `orphan` classification.
12
+ * `alive` worktrees are actively worked on (often large `node_modules`
13
+ * trees) and are never removable, and `broken`/`unrecognized` rows need
14
+ * manual review regardless of size, so walking their full tree would burn
15
+ * significant time for no decision-relevant signal. `"unknown"` also
16
+ * covers a measurement failure/timeout on an orphan candidate.
17
+ */
18
+ readonly sizeBytes: number | "unknown";
19
+ readonly removable: boolean;
20
+ readonly reason: string;
21
+ }
22
+ export interface OrphanClassifyDeps {
23
+ readonly resolveRepoRoot: (candidate: OrphanScanCandidate) => string | undefined;
24
+ readonly repoRootExists: (repoRoot: string) => boolean;
25
+ readonly buildInventory: (repoRoot: string) => WorktreeInventory;
26
+ readonly probeDirty: (path: string) => boolean | "unknown";
27
+ readonly measureSize: (path: string) => number;
28
+ }
29
+ /** Classify a single on-disk candidate. Pure aside from the injected deps. */
30
+ export declare function classifyOrphanCandidate(candidate: OrphanScanCandidate, deps: OrphanClassifyDeps): OrphanScanRow;
31
+ /** Namespace-keyed lookup of a known-live `repoRoot`, first-registered-wins. */
32
+ export declare function buildRegistryRepoRootMap(entries: readonly {
33
+ readonly repoNamespace: string;
34
+ readonly repoRoot: string;
35
+ }[]): ReadonlyMap<string, string>;
36
+ /**
37
+ * Fallback primary-repo resolver for candidates the registry never recorded:
38
+ * a linked worktree's `.git` is a file containing `gitdir: <repoRoot>/.git/
39
+ * worktrees/<name>`. Returns `undefined` when unreadable or unparseable
40
+ * (e.g. `.git` is a directory, or the pointer target was already reaped).
41
+ */
42
+ export declare function resolveRepoRootFromGitPointer(path: string, readGitPointer: (gitPath: string) => string | undefined): string | undefined;
43
+ export interface DiscoverOrphanCandidatesOptions {
44
+ readonly repoNamespaceFilter?: string;
45
+ readonly dirExists?: (path: string) => boolean;
46
+ }
47
+ /** Walk every known managed-worktree root and collect worktree-shaped leaf dirs. */
48
+ export declare function discoverOrphanScanCandidates(roots: readonly string[], options?: DiscoverOrphanCandidatesOptions): OrphanScanCandidate[];
49
+ /** Bounded recursive directory size, symlinks excluded, unreadable entries skipped. */
50
+ export declare function measureDirectorySizeBytes(path: string): number;