@webpresso/agent-kit 3.1.30 → 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 (257) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +179 -19
  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/ci-guardrails-wiring.d.ts +22 -0
  17. package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
  18. package/dist/esm/audit/ci-test-perf.js +30 -0
  19. package/dist/esm/audit/command-surface.js +4 -2
  20. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  21. package/dist/esm/audit/host-substitution-risk.js +256 -0
  22. package/dist/esm/audit/registry.d.ts +4 -4
  23. package/dist/esm/audit/registry.js +8 -1
  24. package/dist/esm/audit/toolchain-isolation.js +1 -2
  25. package/dist/esm/audit/typescript-version.d.ts +2 -0
  26. package/dist/esm/audit/typescript-version.js +123 -0
  27. package/dist/esm/blueprint/core/parser.js +43 -1
  28. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  29. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  30. package/dist/esm/blueprint/core/validation/state.js +25 -14
  31. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  32. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  33. package/dist/esm/blueprint/lifecycle/audit.js +65 -10
  34. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  35. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  36. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  37. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  38. package/dist/esm/blueprint/lifecycle/review-provenance.js +36 -11
  39. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  40. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  41. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  42. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  43. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  44. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  45. package/dist/esm/blueprint/trust/gates.js +20 -0
  46. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  47. package/dist/esm/blueprint/trust/promotion.js +62 -112
  48. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  49. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  50. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  51. package/dist/esm/blueprint/trust/validator.js +7 -1
  52. package/dist/esm/blueprint/utils/archive.js +27 -5
  53. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  54. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  55. package/dist/esm/build/cli-mcp-parity.js +10 -0
  56. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  57. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  58. package/dist/esm/cli/cli.d.ts +1 -1
  59. package/dist/esm/cli/cli.js +34 -0
  60. package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
  61. package/dist/esm/cli/commands/agent-launch.js +82 -4
  62. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  63. package/dist/esm/cli/commands/audit-core.js +8 -1
  64. package/dist/esm/cli/commands/audit.js +17 -5
  65. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  66. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  67. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  68. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  69. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  70. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  71. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  72. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  73. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  74. package/dist/esm/cli/commands/ci-preflight.js +6 -3
  75. package/dist/esm/cli/commands/compile.js +5 -31
  76. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  77. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  78. package/dist/esm/cli/commands/doctor.js +5 -0
  79. package/dist/esm/cli/commands/hook.d.ts +5 -0
  80. package/dist/esm/cli/commands/hook.js +15 -9
  81. package/dist/esm/cli/commands/init/config.js +2 -2
  82. package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
  83. package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
  84. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  85. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  86. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  87. package/dist/esm/cli/commands/init/convergence.js +57 -0
  88. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  89. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  90. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  91. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  92. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  93. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  94. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  95. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  96. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  97. package/dist/esm/cli/commands/init/index.d.ts +1 -0
  98. package/dist/esm/cli/commands/init/index.js +326 -173
  99. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  100. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  101. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  102. package/dist/esm/cli/commands/init/merge.js +30 -1
  103. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  104. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  105. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  106. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  107. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
  108. package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
  109. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  110. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  111. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  112. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
  113. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  114. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  115. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
  116. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
  117. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  118. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  119. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  120. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  121. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  122. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  123. package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
  124. package/dist/esm/cli/commands/opencode-rotate.js +135 -0
  125. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  126. package/dist/esm/cli/commands/public-readiness.js +42 -0
  127. package/dist/esm/cli/commands/review.d.ts +35 -3
  128. package/dist/esm/cli/commands/review.js +729 -209
  129. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  130. package/dist/esm/cli/commands/run-wait.js +122 -0
  131. package/dist/esm/cli/commands/setup-help.js +1 -1
  132. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  133. package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
  134. package/dist/esm/cli/direct-provider-launch.js +9 -5
  135. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  136. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  137. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  138. package/dist/esm/daemon/domains/git-actions.js +136 -0
  139. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  140. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  141. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  142. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  143. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  144. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  145. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  146. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  147. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  148. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  149. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  150. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  151. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  152. package/dist/esm/daemon/handlers/product.js +10 -119
  153. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  154. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  155. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  156. package/dist/esm/daemon/handlers/shared.js +16 -0
  157. package/dist/esm/daemon/methods.d.ts +8 -1
  158. package/dist/esm/daemon/methods.js +7 -0
  159. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  160. package/dist/esm/daemon/protocol/contract.js +26 -0
  161. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  162. package/dist/esm/daemon/protocol/index.js +1 -1
  163. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  164. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  165. package/dist/esm/hooks/conformance/batch.js +5 -0
  166. package/dist/esm/hooks/doctor.d.ts +3 -3
  167. package/dist/esm/hooks/doctor.js +30 -15
  168. package/dist/esm/hooks/permission-request/index.js +9 -3
  169. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  170. package/dist/esm/hooks/permission-request/policy.js +13 -0
  171. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  172. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  173. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  174. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  175. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  176. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  177. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  178. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  179. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  180. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  181. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  182. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  183. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  184. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  185. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  186. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  187. package/dist/esm/hooks/shared/types.d.ts +8 -5
  188. package/dist/esm/hooks/shared/types.js +23 -8
  189. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  190. package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
  191. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  192. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
  193. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  194. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  195. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  196. package/dist/esm/mcp/blueprint/registration.js +3 -2
  197. package/dist/esm/mcp/server.d.ts +1 -0
  198. package/dist/esm/mcp/server.js +27 -0
  199. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  200. package/dist/esm/mcp/tools/_names.js +5 -0
  201. package/dist/esm/mcp/tools/_registry.js +10 -0
  202. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  203. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  204. package/dist/esm/mcp/tools/audit.d.ts +4 -1
  205. package/dist/esm/mcp/tools/audit.js +30 -0
  206. package/dist/esm/mcp/tools/audits.d.ts +3 -0
  207. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  208. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  209. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  210. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  211. package/dist/esm/mcp/tools/review-run.js +216 -0
  212. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  213. package/dist/esm/mcp/tools/run-wait.js +593 -0
  214. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  215. package/dist/esm/mcp/tools/session-id.js +45 -0
  216. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  217. package/dist/esm/mcp/tools/session-info.js +49 -0
  218. package/dist/esm/output-transforms/audit.d.ts +23 -0
  219. package/dist/esm/output-transforms/audit.js +47 -0
  220. package/dist/esm/output-transforms/index.js +2 -0
  221. package/dist/esm/review/authority.js +13 -5
  222. package/dist/esm/review/availability.d.ts +22 -0
  223. package/dist/esm/review/availability.js +107 -20
  224. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  225. package/dist/esm/review/delivery-verifier.js +115 -0
  226. package/dist/esm/review/events.d.ts +3 -2
  227. package/dist/esm/review/events.js +23 -3
  228. package/dist/esm/review/execution/adapters.js +26 -9
  229. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  230. package/dist/esm/review/execution/output-policy.js +12 -0
  231. package/dist/esm/review/execution/review-checkout.js +87 -6
  232. package/dist/esm/review/execution/supervisor.js +5 -0
  233. package/dist/esm/review/execution/types.d.ts +8 -0
  234. package/dist/esm/review/opencode-account.d.ts +24 -0
  235. package/dist/esm/review/opencode-account.js +68 -0
  236. package/dist/esm/review/opencode-auth.d.ts +33 -0
  237. package/dist/esm/review/opencode-auth.js +79 -0
  238. package/dist/esm/review/subject.d.ts +12 -0
  239. package/dist/esm/review/subject.js +74 -22
  240. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  241. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  242. package/dist/esm/status/snapshot.js +30 -19
  243. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  244. package/dist/esm/test/duration-sequencer.js +54 -0
  245. package/dist/esm/test/shard-durations.json +922 -0
  246. package/dist/esm/test/worker-budget.d.ts +6 -0
  247. package/dist/esm/test/worker-budget.js +59 -1
  248. package/dist/esm/test-helpers/global-setup.d.ts +1 -1
  249. package/dist/esm/test-helpers/global-setup.js +68 -24
  250. package/dist/esm/ultragoal/service.d.ts +7 -0
  251. package/dist/esm/ultragoal/service.js +42 -2
  252. package/dist/esm/utils/write-json-file.js +3 -1
  253. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  254. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  255. package/dist/esm/worktrees/registry.d.ts +8 -0
  256. package/dist/esm/worktrees/registry.js +18 -3
  257. package/package.json +16 -13
@@ -0,0 +1,23 @@
1
+ import type { TransformContext, TransformResult } from "./index.js";
2
+ /**
3
+ * `wp audit <kind>` writes the full `formatRepoAuditReport` output (title +
4
+ * one bullet per violation) into its CLI log. Every audit kind normalizes to
5
+ * the shared `"audit"` tool name (see `normalizeToolName`), so without a
6
+ * dedicated transform that output fell through to `genericTransform`, whose
7
+ * error/fail keyword heuristic drops any violation bullet that doesn't
8
+ * literally contain "error"/"fail"/"✗"/"✘" — silently discarding most
9
+ * violation detail even when the full report is a handful of short lines.
10
+ *
11
+ * This transform recognizes the bullet-list shape directly so violation text
12
+ * survives regardless of wording, then defers to the same size-based
13
+ * clip/elision policy every other transform uses (`createTransformResult` /
14
+ * `clipRawOutput`'s existing byte threshold) to decide whether the (now
15
+ * correctly extracted) violation list is short enough to print inline or
16
+ * long enough to still defer to `wp logs audit`.
17
+ *
18
+ * Audit output that isn't shaped like a violation-bullet report (for example
19
+ * raw subprocess output from `mutation`/`bundle-budget`) has no matching
20
+ * bullet lines, so this falls back to the previous generic keyword-based
21
+ * behavior unchanged.
22
+ */
23
+ export declare function auditTransform(rawOutput: string | undefined, context: TransformContext): TransformResult;
@@ -0,0 +1,47 @@
1
+ import { createTransformResult } from "./metadata.js";
2
+ import { genericTransform } from "./generic.js";
3
+ // `formatRepoAuditReport` (src/audit/repo-guardrails.ts) always renders one
4
+ // violation per line as `- <location><metadata><message><fixability>`. That
5
+ // bullet marker is the one structural signal every repo-audit report shares,
6
+ // regardless of which audit kind produced it or whether the violation text
7
+ // happens to contain the words "error"/"fail" that the generic fallback
8
+ // keys off of.
9
+ const VIOLATION_LINE_RE = /^-\s+(.+)$/u;
10
+ /**
11
+ * `wp audit <kind>` writes the full `formatRepoAuditReport` output (title +
12
+ * one bullet per violation) into its CLI log. Every audit kind normalizes to
13
+ * the shared `"audit"` tool name (see `normalizeToolName`), so without a
14
+ * dedicated transform that output fell through to `genericTransform`, whose
15
+ * error/fail keyword heuristic drops any violation bullet that doesn't
16
+ * literally contain "error"/"fail"/"✗"/"✘" — silently discarding most
17
+ * violation detail even when the full report is a handful of short lines.
18
+ *
19
+ * This transform recognizes the bullet-list shape directly so violation text
20
+ * survives regardless of wording, then defers to the same size-based
21
+ * clip/elision policy every other transform uses (`createTransformResult` /
22
+ * `clipRawOutput`'s existing byte threshold) to decide whether the (now
23
+ * correctly extracted) violation list is short enough to print inline or
24
+ * long enough to still defer to `wp logs audit`.
25
+ *
26
+ * Audit output that isn't shaped like a violation-bullet report (for example
27
+ * raw subprocess output from `mutation`/`bundle-budget`) has no matching
28
+ * bullet lines, so this falls back to the previous generic keyword-based
29
+ * behavior unchanged.
30
+ */
31
+ export function auditTransform(rawOutput, context) {
32
+ if (!rawOutput)
33
+ return {};
34
+ const failures = extractViolationBullets(rawOutput);
35
+ if (failures.length === 0)
36
+ return genericTransform(rawOutput, context);
37
+ return createTransformResult(rawOutput, failures.map((failure) => failure.message).join("\n"), context, { tier: 1, failures });
38
+ }
39
+ function extractViolationBullets(rawOutput) {
40
+ const failures = [];
41
+ for (const line of rawOutput.split(/\r?\n/u)) {
42
+ const message = VIOLATION_LINE_RE.exec(line)?.[1]?.trim();
43
+ if (message)
44
+ failures.push({ message });
45
+ }
46
+ return failures;
47
+ }
@@ -1,3 +1,4 @@
1
+ import { auditTransform } from "./audit.js";
1
2
  import { genericTransform } from "./generic.js";
2
3
  import { oxlintTransform } from "./oxlint.js";
3
4
  import { tscTransform } from "./tsc.js";
@@ -8,6 +9,7 @@ const builtInTransforms = new Map([
8
9
  ["lint-oxlint", oxlintTransform],
9
10
  ["typecheck", tscTransform],
10
11
  ["test", vitestTransform],
12
+ ["audit", auditTransform],
11
13
  ]);
12
14
  const transforms = new Map(builtInTransforms);
13
15
  export function registerTransform(toolName, transform) {
@@ -60,7 +60,7 @@ function assertStableSlug(slug) {
60
60
  }
61
61
  function locateBlueprint(entries, stableSlug) {
62
62
  assertStableSlug(stableSlug);
63
- const suffixPattern = new RegExp(`(?:^|/)${LIFECYCLE_SEGMENTS}/${stableSlug.replaceAll("/", "\\/")}/_overview\\.md$`, "u");
63
+ const suffixPattern = new RegExp(`(?:^|/)${LIFECYCLE_SEGMENTS}/${stableSlug.replaceAll("/", "\\/")}(?:/_overview\\.md|\\.md)$`, "u");
64
64
  const matches = entries.filter((entry) => suffixPattern.test(entry.path) &&
65
65
  REGULAR_MODE_PATTERN.test(entry.mode) &&
66
66
  entry.type === "blob");
@@ -69,6 +69,9 @@ function locateBlueprint(entries, stableSlug) {
69
69
  }
70
70
  return matches[0];
71
71
  }
72
+ function ledgerDirectoryForBlueprintPath(blueprintPath) {
73
+ return blueprintPath.endsWith("/_overview.md") ? path.posix.dirname(blueprintPath) : null;
74
+ }
72
75
  function readBlob(cwd, entry) {
73
76
  if (!REGULAR_MODE_PATTERN.test(entry.mode) || entry.type !== "blob") {
74
77
  throw new Error(`Review authority path is not a regular blob: ${entry.path}`);
@@ -115,14 +118,16 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
115
118
  const commit = resolveReviewCommit(cwd, ref);
116
119
  const entries = parseRefTree(cwd, commit);
117
120
  const overview = locateBlueprint(entries, stableSlug);
118
- const blueprintDirectory = path.posix.dirname(overview.path);
119
- const ledgerPath = path.posix.join(blueprintDirectory, "review-events.jsonl");
120
- const ledger = entries.find((entry) => entry.path === ledgerPath);
121
+ const blueprintDirectory = ledgerDirectoryForBlueprintPath(overview.path);
122
+ const ledgerPath = blueprintDirectory === null ? null : path.posix.join(blueprintDirectory, "review-events.jsonl");
123
+ const ledger = ledgerPath === null ? undefined : entries.find((entry) => entry.path === ledgerPath);
121
124
  const events = ledger ? parseReviewEventLog(readBlob(cwd, ledger).toString("utf8")) : [];
122
125
  if (events.some((event) => event.blueprintSlug !== stableSlug)) {
123
126
  throw new Error(`Review event ledger contains an event for a different blueprint: ${ledgerPath}`);
124
127
  }
125
- const { compromisedIds, issues } = markCompromisedArtifactEvents(cwd, entries, blueprintDirectory, events);
128
+ const { compromisedIds, issues } = blueprintDirectory === null
129
+ ? { compromisedIds: new Set(), issues: [] }
130
+ : markCompromisedArtifactEvents(cwd, entries, blueprintDirectory, events);
126
131
  const subject = purpose === "plan"
127
132
  ? createPlanSubjectAtRef(cwd, commit, stableSlug, overview.path)
128
133
  : createDeliverySubjectAtRef(cwd, commit, stableSlug, overview.path);
@@ -160,6 +165,9 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
160
165
  };
161
166
  }
162
167
  export function readWorkingReviewEvents(blueprintPath, stableSlug) {
168
+ if (path.basename(blueprintPath) !== "_overview.md") {
169
+ return { events: [], compromisedIds: new Set(), issues: [] };
170
+ }
163
171
  const ledgerPath = path.join(path.dirname(blueprintPath), "review-events.jsonl");
164
172
  if (!existsSync(ledgerPath))
165
173
  return { events: [], compromisedIds: new Set(), issues: [] };
@@ -49,3 +49,25 @@ export declare function pruneExpiredAvailabilitySuppressions(state: ReviewAvaila
49
49
  state: ReviewAvailabilityState;
50
50
  pruned: string[];
51
51
  };
52
+ export declare const OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION = 1;
53
+ export interface OpencodeAccountAvailabilityState {
54
+ readonly schemaVersion: 1;
55
+ readonly updatedAt: string;
56
+ readonly accounts: Record<string, ReviewAvailabilityRecord>;
57
+ }
58
+ export interface OpencodeAccountAvailabilityEvent {
59
+ readonly limitWindow?: ReviewLimitWindow;
60
+ readonly retryAfterMs?: number;
61
+ readonly failureCode?: string;
62
+ readonly terminalClassification?: string;
63
+ readonly at?: string;
64
+ }
65
+ export declare function opencodeAccountAvailabilityPath(): string;
66
+ export declare function loadOpencodeAccountAvailability(): OpencodeAccountAvailabilityState;
67
+ export declare function isOpencodeAccountSuppressed(state: OpencodeAccountAvailabilityState, canonicalId: string): boolean;
68
+ export declare function recordOpencodeAccountAvailability(canonicalId: string, event: OpencodeAccountAvailabilityEvent): OpencodeAccountAvailabilityState;
69
+ export declare function recordOpencodeAccountAvailabilityWithLock(canonicalId: string, event: OpencodeAccountAvailabilityEvent): Promise<OpencodeAccountAvailabilityState>;
70
+ export declare function pruneExpiredOpencodeAccountSuppressions(state: OpencodeAccountAvailabilityState, nowMs?: number): {
71
+ state: OpencodeAccountAvailabilityState;
72
+ pruned: string[];
73
+ };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { NotInGitRepoError, withLock } from "#paths/state-root.js";
3
+ import { getStateRoot, NotInGitRepoError, withLock } from "#paths/state-root.js";
4
4
  import { writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
5
5
  export const REVIEW_AVAILABILITY_SCHEMA_VERSION = 1;
6
6
  const FAILURE_CLASSIFICATIONS = new Set(["provider-limit-reached", "provider-unavailable"]);
@@ -121,10 +121,7 @@ function isAvailabilitySuccess(event) {
121
121
  return SUCCESS_CLASSIFICATIONS.has(event.terminalClassification);
122
122
  }
123
123
  function isProviderWideOpenCodeFailure(event) {
124
- return (event.provider === "opencode" &&
125
- (event.failureCode === "opencode-go-usage-limit" ||
126
- event.reviewer === undefined ||
127
- event.reviewer === "opencode"));
124
+ return (event.provider === "opencode" && (event.reviewer === undefined || event.reviewer === "opencode"));
128
125
  }
129
126
  function nextRecord(previous, event, input) {
130
127
  const at = event.at ?? new Date().toISOString();
@@ -250,26 +247,35 @@ export function suppressedReviewers(state) {
250
247
  .filter((record) => record.suppressed)
251
248
  .map((record) => record.canonicalId));
252
249
  }
253
- export function pruneExpiredAvailabilitySuppressions(state, nowMs = Date.now()) {
254
- const pruned = [];
255
- const pruneMap = (records) => Object.fromEntries(Object.entries(records).map(([key, record]) => {
256
- if (record.suppressed && !suppressedRecordStillApplies(record)) {
250
+ /**
251
+ * Shared expiry predicate for a suppressed availability record: applies to
252
+ * both the reviewer/provider store and the opencode-go account store below.
253
+ * A record clears when its terminal classification is no longer a failure
254
+ * classification, or when its retry window has elapsed.
255
+ */
256
+ function suppressionExpired(record, nowMs) {
257
+ if (!record.suppressed)
258
+ return false;
259
+ if (!suppressedRecordStillApplies(record))
260
+ return true;
261
+ if (record.retryAfterMs === undefined || record.lastFailureAt === undefined)
262
+ return false;
263
+ const failureAt = Date.parse(record.lastFailureAt);
264
+ return Number.isFinite(failureAt) && failureAt + record.retryAfterMs <= nowMs;
265
+ }
266
+ function pruneRecordMap(records, nowMs, pruned) {
267
+ return Object.fromEntries(Object.entries(records).map(([key, record]) => {
268
+ if (suppressionExpired(record, nowMs)) {
257
269
  pruned.push(key);
258
270
  return [key, { ...record, suppressed: false }];
259
271
  }
260
- if (record.suppressed &&
261
- record.retryAfterMs !== undefined &&
262
- record.lastFailureAt !== undefined) {
263
- const failureAt = Date.parse(record.lastFailureAt);
264
- if (Number.isFinite(failureAt) && failureAt + record.retryAfterMs <= nowMs) {
265
- pruned.push(key);
266
- return [key, { ...record, suppressed: false }];
267
- }
268
- }
269
272
  return [key, record];
270
273
  }));
271
- const providers = pruneMap(state.providers);
272
- const reviewers = pruneMap(state.reviewers);
274
+ }
275
+ export function pruneExpiredAvailabilitySuppressions(state, nowMs = Date.now()) {
276
+ const pruned = [];
277
+ const providers = pruneRecordMap(state.providers, nowMs, pruned);
278
+ const reviewers = pruneRecordMap(state.reviewers, nowMs, pruned);
273
279
  if (pruned.length === 0)
274
280
  return { state, pruned };
275
281
  return {
@@ -282,3 +288,84 @@ export function pruneExpiredAvailabilitySuppressions(state, nowMs = Date.now())
282
288
  pruned,
283
289
  };
284
290
  }
291
+ // --- opencode-go multi-account cooldown store (user-scoped, cross-repo) ---
292
+ export const OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION = 1;
293
+ export function opencodeAccountAvailabilityPath() {
294
+ return path.join(getStateRoot(), "opencode-account-availability.json");
295
+ }
296
+ function emptyOpencodeAccountAvailability(now = new Date().toISOString()) {
297
+ return {
298
+ schemaVersion: OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION,
299
+ updatedAt: now,
300
+ accounts: {},
301
+ };
302
+ }
303
+ export function loadOpencodeAccountAvailability() {
304
+ const file = opencodeAccountAvailabilityPath();
305
+ if (!existsSync(file))
306
+ return emptyOpencodeAccountAvailability();
307
+ try {
308
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
309
+ if (!isRecord(parsed) ||
310
+ parsed.schemaVersion !== OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION) {
311
+ return emptyOpencodeAccountAvailability();
312
+ }
313
+ return {
314
+ schemaVersion: OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION,
315
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
316
+ accounts: normalizeRecordMap(parsed.accounts),
317
+ };
318
+ }
319
+ catch {
320
+ return emptyOpencodeAccountAvailability();
321
+ }
322
+ }
323
+ function saveOpencodeAccountAvailability(state) {
324
+ const file = opencodeAccountAvailabilityPath();
325
+ mkdirSync(path.dirname(file), { recursive: true });
326
+ writeJsonFileAtomic(file, state);
327
+ }
328
+ export function isOpencodeAccountSuppressed(state, canonicalId) {
329
+ return state.accounts[canonicalId]?.suppressed === true;
330
+ }
331
+ export function recordOpencodeAccountAvailability(canonicalId, event) {
332
+ const at = event.at ?? new Date().toISOString();
333
+ const current = loadOpencodeAccountAvailability();
334
+ const accounts = { ...current.accounts };
335
+ accounts[canonicalId] = {
336
+ canonicalId,
337
+ provider: "opencode",
338
+ terminalClassification: event.terminalClassification ?? "provider-limit-reached",
339
+ ...(event.failureCode ? { failureCode: event.failureCode } : {}),
340
+ ...(event.limitWindow ? { limitWindow: event.limitWindow } : {}),
341
+ ...(event.retryAfterMs === undefined ? {} : { retryAfterMs: event.retryAfterMs }),
342
+ lastFailureAt: at,
343
+ suppressed: true,
344
+ suppressionScope: "provider",
345
+ suppressedAt: at,
346
+ };
347
+ const next = {
348
+ schemaVersion: OPENCODE_ACCOUNT_AVAILABILITY_SCHEMA_VERSION,
349
+ updatedAt: at,
350
+ accounts,
351
+ };
352
+ saveOpencodeAccountAvailability(next);
353
+ return next;
354
+ }
355
+ export function recordOpencodeAccountAvailabilityWithLock(canonicalId, event) {
356
+ return withLock("user", () => recordOpencodeAccountAvailability(canonicalId, event));
357
+ }
358
+ export function pruneExpiredOpencodeAccountSuppressions(state, nowMs = Date.now()) {
359
+ const pruned = [];
360
+ const accounts = pruneRecordMap(state.accounts, nowMs, pruned);
361
+ if (pruned.length === 0)
362
+ return { state, pruned };
363
+ return {
364
+ state: {
365
+ ...state,
366
+ updatedAt: new Date(nowMs).toISOString(),
367
+ accounts,
368
+ },
369
+ pruned,
370
+ };
371
+ }
@@ -0,0 +1,27 @@
1
+ import { executeAllowedWpCommand } from "#trust/command-runner.js";
2
+ import type { ReviewTargetCheckout } from "#review/execution/review-checkout.js";
3
+ export interface ReviewGateVerificationCommand {
4
+ readonly command: string;
5
+ readonly outcome: "passed" | "failed" | "timed-out" | "deferred";
6
+ readonly durationMs: number;
7
+ readonly exitCode: number | null;
8
+ readonly signal?: string;
9
+ }
10
+ export interface ReviewGateVerification {
11
+ readonly status: "passed" | "failed";
12
+ readonly commands: readonly ReviewGateVerificationCommand[];
13
+ readonly failureCode?: string;
14
+ readonly logPath?: string;
15
+ }
16
+ export interface VerifyDeliveryGatesInput {
17
+ readonly purpose: "plan" | "delivery";
18
+ readonly blueprintContent: string;
19
+ readonly projectRoot: string;
20
+ readonly gateId: string;
21
+ readonly checkout: ReviewTargetCheckout;
22
+ readonly signal?: AbortSignal;
23
+ readonly executeCommand?: typeof executeAllowedWpCommand;
24
+ }
25
+ export declare function verifyDeliveryPromotionGates(input: VerifyDeliveryGatesInput): ReviewGateVerification | undefined;
26
+ export declare function verificationPromptSummary(verification: ReviewGateVerification | undefined): string | undefined;
27
+ export declare function verificationArtifactSection(verification: ReviewGateVerification | undefined): string;
@@ -0,0 +1,115 @@
1
+ import path from "node:path";
2
+ import { executeAllowedWpCommand, relativeCommandLogPath, } from "#trust/command-runner.js";
3
+ import { parseTrustDossier } from "#trust/dossier.js";
4
+ import { parseAllowedWpCommand } from "#trust/gates.js";
5
+ export function verifyDeliveryPromotionGates(input) {
6
+ if (input.purpose !== "delivery")
7
+ return undefined;
8
+ const parsed = parseTrustDossier(input.blueprintContent);
9
+ if (parsed.violations.length > 0) {
10
+ if (parsed.violations.length === 1 &&
11
+ parsed.violations[0]?.section === "Trust Dossier" &&
12
+ parsed.violations[0]?.message === "missing Trust Dossier section") {
13
+ return undefined;
14
+ }
15
+ return {
16
+ status: "failed",
17
+ commands: [],
18
+ failureCode: "malformed-trust-dossier",
19
+ };
20
+ }
21
+ const gates = parsed.dossier?.gates ?? [];
22
+ if (gates.length === 0)
23
+ return undefined;
24
+ const commands = [];
25
+ const executeCommand = input.executeCommand ?? executeAllowedWpCommand;
26
+ const logRoot = path.join(input.projectRoot, ".webpresso", "logs", "delivery-verification", input.gateId);
27
+ for (const gate of gates) {
28
+ if (input.signal?.aborted) {
29
+ return {
30
+ status: "failed",
31
+ commands,
32
+ failureCode: "verification-cancelled",
33
+ };
34
+ }
35
+ if (isApprovalDependentLifecycleGate(gate.command)) {
36
+ commands.push({
37
+ command: gate.command,
38
+ outcome: "deferred",
39
+ durationMs: 0,
40
+ exitCode: null,
41
+ });
42
+ continue;
43
+ }
44
+ const result = executeCommand({
45
+ command: gate.command,
46
+ executionRoot: input.checkout.cwd,
47
+ logRoot,
48
+ env: { CI: "1", NO_COLOR: "1", WP_FORCE_SOURCE: "1" },
49
+ });
50
+ commands.push(publicCommandResult(result));
51
+ if (result.outcome !== "passed") {
52
+ return {
53
+ status: "failed",
54
+ commands,
55
+ failureCode: result.failureCode ?? "verification-command-failed",
56
+ logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
57
+ };
58
+ }
59
+ const changes = input.checkout.changes();
60
+ if (changes.length > 0) {
61
+ return {
62
+ status: "failed",
63
+ commands,
64
+ failureCode: "verification-worktree-contaminated",
65
+ logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
66
+ };
67
+ }
68
+ }
69
+ return {
70
+ status: "passed",
71
+ commands,
72
+ };
73
+ }
74
+ export function verificationPromptSummary(verification) {
75
+ if (!verification)
76
+ return undefined;
77
+ if (verification.status !== "passed")
78
+ return undefined;
79
+ return [
80
+ "Host verification before outside review: passed.",
81
+ ...verification.commands.map((command, index) => `${index + 1}. ${command.command}: ${command.outcome === "deferred" ? "deferred until review publication" : "passed"}`),
82
+ ].join("\n");
83
+ }
84
+ function isApprovalDependentLifecycleGate(command) {
85
+ try {
86
+ const [, subcommand, kind] = parseAllowedWpCommand(command);
87
+ return subcommand === "audit" && kind === "blueprint-lifecycle";
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ export function verificationArtifactSection(verification) {
94
+ if (!verification)
95
+ return "";
96
+ const lines = [
97
+ "## Host Verification",
98
+ "",
99
+ `Status: ${verification.status}`,
100
+ ...verification.commands.map((command, index) => `${index + 1}. ${command.command} -> ${command.outcome} (${command.durationMs}ms, exit ${command.exitCode ?? "null"})`),
101
+ ...(verification.failureCode ? [`Failure code: ${verification.failureCode}`] : []),
102
+ ...(verification.logPath ? [`Log: ${verification.logPath}`] : []),
103
+ "",
104
+ ];
105
+ return `${lines.join("\n")}\n`;
106
+ }
107
+ function publicCommandResult(result) {
108
+ return {
109
+ command: result.command,
110
+ outcome: result.outcome,
111
+ durationMs: result.durationMs,
112
+ exitCode: result.exitCode,
113
+ ...(result.signal ? { signal: result.signal } : {}),
114
+ };
115
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ export declare const MAINTAINER_OVERRIDE_REVIEWER = "maintainer-override";
2
3
  export declare const reviewEventSchema: z.ZodObject<{
3
4
  schemaVersion: z.ZodLiteral<2>;
4
5
  id: z.ZodString;
@@ -16,12 +17,12 @@ export declare const reviewEventSchema: z.ZodObject<{
16
17
  gateId: z.ZodString;
17
18
  promptHash: z.ZodString;
18
19
  reviewer: z.ZodString;
19
- provider: z.ZodEnum<{
20
+ provider: z.ZodOptional<z.ZodEnum<{
20
21
  claude: "claude";
21
22
  codex: "codex";
22
23
  grok: "grok";
23
24
  opencode: "opencode";
24
- }>;
25
+ }>>;
25
26
  model: z.ZodOptional<z.ZodString>;
26
27
  effort: z.ZodOptional<z.ZodEnum<{
27
28
  high: "high";
@@ -9,6 +9,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
9
9
  const COMMIT_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
10
10
  const BLUEPRINT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/u;
11
11
  const ARTIFACT_PATH_PATTERN = /^review-artifacts\/[a-zA-Z0-9._-]+\.md$/u;
12
+ // The honest, auditable escape hatch reviewer id. A maintainer override is a
13
+ // HUMAN adjudication (not a machine backend), recorded only by `wp review
14
+ // override` after a real strong-reviewer rejection of the same subject. It
15
+ // satisfies the promotion gate DISTINCTLY (never folded into a strong or
16
+ // OpenCode-Go approval). Defined here — the lowest review-core module — so both
17
+ // the ledger read-time policy below and the lifecycle approval gate share one
18
+ // source of truth without an import cycle.
19
+ export const MAINTAINER_OVERRIDE_REVIEWER = "maintainer-override";
12
20
  export const reviewEventSchema = z
13
21
  .object({
14
22
  schemaVersion: z.literal(2),
@@ -21,7 +29,10 @@ export const reviewEventSchema = z
21
29
  gateId: z.string().min(1).max(120),
22
30
  promptHash: z.string().regex(SHA256_PATTERN),
23
31
  reviewer: z.string().min(1).max(120),
24
- provider: z.enum(["claude", "codex", "opencode", "grok"]),
32
+ // Optional: present for machine-backend reviews (always set by `wp review
33
+ // gate`/`log`); absent for a human maintainer-override, which has no AI
34
+ // backend. Descriptive metadata only — no policy keys off `provider`.
35
+ provider: z.enum(["claude", "codex", "opencode", "grok"]).optional(),
25
36
  model: z.string().min(1).max(240).optional(),
26
37
  effort: z.enum(["medium", "high"]).optional(),
27
38
  stage: z.string().min(1).max(120),
@@ -81,7 +92,7 @@ function orderedEvent(event) {
81
92
  gateId: event.gateId,
82
93
  promptHash: event.promptHash,
83
94
  reviewer: event.reviewer,
84
- provider: event.provider,
95
+ ...(event.provider ? { provider: event.provider } : {}),
85
96
  ...(event.model ? { model: event.model } : {}),
86
97
  ...(event.effort ? { effort: event.effort } : {}),
87
98
  stage: event.stage,
@@ -164,10 +175,19 @@ function isManualMarker(value) {
164
175
  // dodge the policy while real gate ids (content hashes) can never collide.
165
176
  return value.trim().toLowerCase() === "manual";
166
177
  }
178
+ // Reviewers whose manual-stage approvals are legitimate governance decisions,
179
+ // not disguised machine approvals: a plain `human` review and a
180
+ // `maintainer-override` (a maintainer's hand-recorded adjudication). Both are
181
+ // human authority by construction, so a manual marker on their events is
182
+ // expected rather than a policy dodge.
183
+ const HUMAN_LIKE_REVIEWERS = new Set(["human", MAINTAINER_OVERRIDE_REVIEWER]);
184
+ function isHumanLikeReviewer(reviewer) {
185
+ return HUMAN_LIKE_REVIEWERS.has(canonicalReviewer(reviewer));
186
+ }
167
187
  function isManualMachineApproval(event) {
168
188
  return (isPolicyApprovalVerdict(event.verdict) &&
169
189
  (isManualMarker(event.gateId) || isManualMarker(event.stage)) &&
170
- canonicalReviewer(event.reviewer) !== "human");
190
+ !isHumanLikeReviewer(event.reviewer));
171
191
  }
172
192
  // `compromisedIds` marks events whose artifact evidence failed integrity
173
193
  // verification. They still PARTICIPATE in reviewer supersession — a later
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { CLAUDE_DEFAULT_REVIEW_MODEL, CODEX_DEFAULT_REVIEW_MODEL, GROK_DEFAULT_REVIEW_MODEL, } from "./types.js";
6
- import { canonicalizeProviderFailureCode } from "./output-policy.js";
6
+ import { canonicalizeProviderFailureCode, sanitizeProviderErrorDetail } from "./output-policy.js";
7
7
  function objectValue(value) {
8
8
  return value !== null && typeof value === "object" && !Array.isArray(value)
9
9
  ? value
@@ -61,6 +61,18 @@ function providerErrorMessage(value) {
61
61
  ];
62
62
  return candidates.find((candidate) => typeof candidate === "string") ?? null;
63
63
  }
64
+ // Builds a "failure" event, attaching the provider's raw diagnostic text
65
+ // (sanitized and length-bounded) alongside the canonical short `code` when
66
+ // one is available. `code` stays the only field the retry/classification
67
+ // control flow reads; `message` exists purely to be surfaced to a caller who
68
+ // is stuck debugging a generic failure code.
69
+ function providerFailureEvent(value, code, message = providerErrorMessage(value)) {
70
+ return {
71
+ kind: "failure",
72
+ code,
73
+ ...(message ? { message: sanitizeProviderErrorDetail(message) } : {}),
74
+ };
75
+ }
64
76
  function parseResetDurationMs(message) {
65
77
  const reset = message.match(/\bresets?\s+in\s+([^.'"]+)/iu)?.[1];
66
78
  if (!reset)
@@ -94,6 +106,7 @@ function opencodeUsageLimit(message) {
94
106
  return {
95
107
  kind: "failure",
96
108
  code: "opencode-go-usage-limit",
109
+ message: sanitizeProviderErrorDetail(message),
97
110
  limitWindow,
98
111
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
99
112
  };
@@ -104,7 +117,7 @@ function claudeEvent(value) {
104
117
  return { kind: "unsupported" };
105
118
  if (record.type === "result") {
106
119
  if (record.is_error === true || record.subtype === "error") {
107
- return { kind: "failure", code: failureCode(record) };
120
+ return providerFailureEvent(record, failureCode(record));
108
121
  }
109
122
  return {
110
123
  kind: "terminal",
@@ -112,7 +125,7 @@ function claudeEvent(value) {
112
125
  };
113
126
  }
114
127
  if (record.type === "error")
115
- return { kind: "failure", code: failureCode(record) };
128
+ return providerFailureEvent(record, failureCode(record));
116
129
  if (record.type === "rate_limit_event") {
117
130
  const rateLimit = objectValue(record.rate_limit_info);
118
131
  return rateLimit?.status === "rejected"
@@ -127,7 +140,7 @@ function claudeEvent(value) {
127
140
  }
128
141
  if (record.type === "assistant") {
129
142
  return typeof record.error === "string"
130
- ? { kind: "failure", code: failureCode(record) }
143
+ ? providerFailureEvent(record, failureCode(record), record.error)
131
144
  : { kind: "progress", frontier: claudeEnvelopeFrontier(record) };
132
145
  }
133
146
  if (record.type !== "stream_event")
@@ -151,7 +164,7 @@ function claudeEvent(value) {
151
164
  return { kind: "progress", frontier: stableFrontier(event) };
152
165
  }
153
166
  if (event.type === "error")
154
- return { kind: "failure", code: failureCode(event) };
167
+ return providerFailureEvent(event, failureCode(event));
155
168
  return { kind: "unsupported" };
156
169
  }
157
170
  function opencodeText(record) {
@@ -216,7 +229,7 @@ function opencodeEvent(value) {
216
229
  if (record.type === "error") {
217
230
  const message = providerErrorMessage(record);
218
231
  const usageLimit = message === null ? null : opencodeUsageLimit(message);
219
- return usageLimit ?? { kind: "failure", code: failureCode(record) };
232
+ return usageLimit ?? providerFailureEvent(record, failureCode(record), message);
220
233
  }
221
234
  // OpenCode's non-interactive JSON protocol closes a completed run with
222
235
  // session.idle or step_finish(reason: "stop"); recorded Kimi and DeepSeek
@@ -293,10 +306,10 @@ function codexEvent(value) {
293
306
  : message && /rate.?limit|quota/iu.test(message)
294
307
  ? "rate-limit-rejected"
295
308
  : "provider-error");
296
- return { kind: "failure", code };
309
+ return providerFailureEvent(record, code, message);
297
310
  }
298
311
  if (type.includes("rate_limit") && /reject|limit|quota/iu.test(message ?? "")) {
299
- return { kind: "failure", code: "rate-limit-rejected" };
312
+ return providerFailureEvent(record, "rate-limit-rejected", message);
300
313
  }
301
314
  if (type === "result" ||
302
315
  type === "final" ||
@@ -399,6 +412,10 @@ export const opencodeReviewAdapter = {
399
412
  OPENCODE_CONFIG_CONTENT: OPENCODE_REVIEW_CONFIG,
400
413
  OPENCODE_DISABLE_AUTOUPDATE: "true",
401
414
  OPENCODE_DISABLE_PRUNE: "true",
415
+ // Isolate the credential store per account: the spawn reads
416
+ // `<authDataDir>/opencode/auth.json` instead of the user's global file,
417
+ // so mid-flight account failover never races a live interactive TUI.
418
+ ...(input.authDataDir ? { XDG_DATA_HOME: input.authDataDir } : {}),
402
419
  },
403
420
  cleanup: attachment.cleanup,
404
421
  };
@@ -506,7 +523,7 @@ export const grokReviewAdapter = {
506
523
  return {
507
524
  kind: "failure",
508
525
  code: "provider-failed",
509
- message,
526
+ message: sanitizeProviderErrorDetail(message),
510
527
  };
511
528
  }
512
529
  return { kind: "progress", frontier: type || "event" };
@@ -11,4 +11,6 @@ export type ReviewOutputPolicyResult = {
11
11
  };
12
12
  export declare function sanitizeReviewOutput(value: string): string;
13
13
  export declare function applyReviewOutputPolicy(value: string, consumedBytes?: number): ReviewOutputPolicyResult;
14
+ export declare const REVIEW_ERROR_DETAIL_MAX_CHARS = 400;
15
+ export declare function sanitizeProviderErrorDetail(value: string): string;
14
16
  export declare function canonicalizeProviderFailureCode(value: unknown): string;