@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
@@ -23,6 +23,18 @@ export function applyReviewOutputPolicy(value, consumedBytes = 0) {
23
23
  }
24
24
  return { ok: true, value: sanitized, bytes, totalBytes: consumedBytes + bytes };
25
25
  }
26
+ export const REVIEW_ERROR_DETAIL_MAX_CHARS = 400;
27
+ // Bounds and redacts a provider's raw diagnostic text (a JSON envelope's
28
+ // error.message field, or a raw stderr line) before it is retained anywhere.
29
+ // This feeds only the ephemeral gate state file and CLI stdout -- never the
30
+ // durable, git-committed review ledger -- but the same secret redaction that
31
+ // already applies to terminal/progress "result" text applies here too.
32
+ export function sanitizeProviderErrorDetail(value) {
33
+ const sanitized = sanitizeReviewOutput(value).replace(/\s+/gu, " ").trim();
34
+ return sanitized.length > REVIEW_ERROR_DETAIL_MAX_CHARS
35
+ ? `${sanitized.slice(0, REVIEW_ERROR_DETAIL_MAX_CHARS)}…`
36
+ : sanitized;
37
+ }
26
38
  export function canonicalizeProviderFailureCode(value) {
27
39
  if (typeof value !== "string")
28
40
  return "provider-error";
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
- import { join } from "node:path";
5
+ import { basename, join } from "node:path";
6
6
  function git(cwd, args, input) {
7
7
  return execFileSync("git", [...args], {
8
8
  cwd,
@@ -60,12 +60,69 @@ function worktreeFingerprint(cwd) {
60
60
  }
61
61
  return hash.digest("hex");
62
62
  }
63
- function status(cwd) {
63
+ // Path segments that mark gitignored content as NON-source: derived build
64
+ // outputs a Promotion Gate command (`./bin/wp typecheck`/`test`/etc.)
65
+ // regenerates while self-hosting this repo (`dist/`, `.tshy/`) and the
66
+ // reconstructed dependency tree (`node_modules`, already separately allowlisted
67
+ // via `ignoredPaths` when symlinked — kept here too for depth-safety, e.g. a
68
+ // nested per-workspace-package `node_modules`). A gitignored (`!!`) path whose
69
+ // ANY segment is one of these — at any workspace depth (e.g. `packages/x/dist`,
70
+ // `.tshy/build.log`) — is exempt from the contamination check. Confirmed
71
+ // empirically that the gate commands emit gitignored output exclusively under
72
+ // these roots.
73
+ //
74
+ // SCOPE (deliberate, documented): this verifier enforces the review TARGET's
75
+ // *source* integrity, not its build artifacts. It does not flag writes under a
76
+ // derived/dependency root, because such output can reach neither the caller
77
+ // nor the reviewed source (the checkout is a disposable tmpdir). Everything
78
+ // else stays strict: a tracked mutation (` M `/` D `), a new untracked source
79
+ // file (`?? `), and any NON-derived gitignored write (e.g. a stray `.env`) all
80
+ // still surface.
81
+ const NON_SOURCE_IGNORED_SEGMENTS = new Set(["dist", ".tshy", "node_modules"]);
82
+ function isNonSourceIgnoredPath(relativePath) {
83
+ return relativePath.split("/").some((segment) => NON_SOURCE_IGNORED_SEGMENTS.has(segment));
84
+ }
85
+ // This repo self-hosts: a Promotion Gate command's first `./bin/wp` invocation
86
+ // triggers wp's own bootstrap build, which runs tshy and then
87
+ // `./bin/wp format --file package.json` — an IDEMPOTENT normalization (e.g.
88
+ // adding a missing trailing newline). Re-running it a second time produces
89
+ // byte-identical output modulo trailing whitespace, so it is not a source
90
+ // mutation. Exempt ONLY that exact shape: a plain, unstaged content
91
+ // modification (` M `) of a file literally named `package.json` whose
92
+ // tracked-vs-working-tree bytes differ solely in trailing whitespace. Any
93
+ // other package.json change (dependency/script/field edits, staged changes,
94
+ // adds, deletes) still fails closed — this check never inspects JSON
95
+ // semantics, only trailing-whitespace-normalized byte equality.
96
+ function isNormalizedPackageJsonModification(cwd, line) {
97
+ if (!line.startsWith(" M "))
98
+ return false;
99
+ const relativePath = line.slice(3);
100
+ if (basename(relativePath) !== "package.json")
101
+ return false;
102
+ try {
103
+ const working = readFileSync(join(cwd, relativePath), "utf8");
104
+ const tracked = git(cwd, ["show", `HEAD:${relativePath}`]);
105
+ return working.replace(/\s+$/u, "") === tracked.replace(/\s+$/u, "");
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ function status(cwd, ignoredPaths = new Set()) {
64
112
  const output = git(cwd, ["status", "--porcelain=v1", "--untracked-files=all", "--ignored"]);
65
113
  return output
66
114
  .split(/\r?\n/u)
67
- .map((line) => line.trimEnd())
68
- .filter((line) => line.length > 0);
115
+ .filter((line) => line.length > 0)
116
+ .filter((line) => {
117
+ const relativePath = line.slice(3).replace(/\/$/u, "");
118
+ if (ignoredPaths.has(relativePath))
119
+ return false;
120
+ if (line.startsWith("!!") && isNonSourceIgnoredPath(relativePath))
121
+ return false;
122
+ if (isNormalizedPackageJsonModification(cwd, line))
123
+ return false;
124
+ return true;
125
+ });
69
126
  }
70
127
  function neutralizeSymlinks(directory) {
71
128
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
@@ -111,6 +168,7 @@ export function createReviewTargetCheckout(repositoryRoot, targetSha) {
111
168
  git(cwd, ["update-ref", "HEAD", baselineHead]);
112
169
  if (status(cwd).length > 0)
113
170
  throw new Error("review target checkout was not clean at start");
171
+ const injectedPaths = projectCallerDependencies(repositoryRoot, cwd);
114
172
  let cleaned = false;
115
173
  return {
116
174
  cwd,
@@ -119,7 +177,7 @@ export function createReviewTargetCheckout(repositoryRoot, targetSha) {
119
177
  },
120
178
  changes: () => {
121
179
  try {
122
- const changes = [...status(cwd)];
180
+ const changes = [...status(cwd, injectedPaths)];
123
181
  if (git(cwd, ["rev-parse", "HEAD"]).trim() !== baselineHead) {
124
182
  changes.push("review checkout HEAD changed");
125
183
  }
@@ -145,3 +203,26 @@ export function createReviewTargetCheckout(repositoryRoot, targetSha) {
145
203
  throw error;
146
204
  }
147
205
  }
206
+ function projectCallerDependencies(repositoryRoot, checkoutRoot) {
207
+ const projected = new Set();
208
+ const dependencyPath = "node_modules";
209
+ const callerPath = join(repositoryRoot, dependencyPath);
210
+ const targetPath = join(checkoutRoot, dependencyPath);
211
+ try {
212
+ lstatSync(callerPath);
213
+ }
214
+ catch {
215
+ return projected;
216
+ }
217
+ try {
218
+ lstatSync(targetPath);
219
+ return projected;
220
+ }
221
+ catch {
222
+ // Missing in the target archive means the caller dependency cache may be
223
+ // projected for local tooling without overwriting target-owned files.
224
+ }
225
+ symlinkSync(callerPath, targetPath, "dir");
226
+ projected.add(dependencyPath);
227
+ return projected;
228
+ }
@@ -107,6 +107,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
107
107
  });
108
108
  let status = null;
109
109
  let failureCode;
110
+ let providerErrorDetail;
110
111
  let limitWindow;
111
112
  let retryAfterMs;
112
113
  let result;
@@ -191,6 +192,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
191
192
  failureCode = code;
192
193
  limitWindow = metadata.limitWindow;
193
194
  retryAfterMs = metadata.retryAfterMs;
195
+ providerErrorDetail = metadata.providerErrorDetail;
194
196
  requestTermination();
195
197
  finalize();
196
198
  };
@@ -249,6 +251,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
249
251
  fail("provider-failure", canonicalizeProviderFailureCode(event.code), {
250
252
  ...(event.limitWindow === undefined ? {} : { limitWindow: event.limitWindow }),
251
253
  ...(event.retryAfterMs === undefined ? {} : { retryAfterMs: event.retryAfterMs }),
254
+ ...(event.message === undefined ? {} : { providerErrorDetail: event.message }),
252
255
  });
253
256
  return;
254
257
  }
@@ -324,6 +327,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
324
327
  fail("provider-failure", canonicalizeProviderFailureCode(event.code), {
325
328
  ...(event.limitWindow === undefined ? {} : { limitWindow: event.limitWindow }),
326
329
  ...(event.retryAfterMs === undefined ? {} : { retryAfterMs: event.retryAfterMs }),
330
+ ...(event.message === undefined ? {} : { providerErrorDetail: event.message }),
327
331
  });
328
332
  }
329
333
  };
@@ -425,6 +429,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
425
429
  markerMatched,
426
430
  ...(result === undefined ? {} : { result }),
427
431
  ...(terminalFailureCode === undefined ? {} : { failureCode: terminalFailureCode }),
432
+ ...(providerErrorDetail === undefined ? {} : { providerErrorDetail }),
428
433
  ...(limitWindow === undefined ? {} : { limitWindow }),
429
434
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
430
435
  summaryPath: artifacts.summaryPath,
@@ -19,6 +19,12 @@ export interface ReviewRunInput {
19
19
  readonly cwd?: string;
20
20
  readonly registryRoot?: string;
21
21
  readonly signal?: AbortSignal;
22
+ /**
23
+ * Isolated opencode auth store directory for this spawn. When set, the
24
+ * opencode adapter injects it as `XDG_DATA_HOME` so the child reads a
25
+ * per-account `auth.json` instead of the user's global credential store.
26
+ */
27
+ readonly authDataDir?: string;
22
28
  }
23
29
  export interface ReviewInvocation {
24
30
  readonly command: string;
@@ -41,6 +47,7 @@ export type ReviewAdapterEvent = {
41
47
  } | {
42
48
  readonly kind: "failure";
43
49
  readonly code: string;
50
+ readonly message?: string;
44
51
  readonly limitWindow?: ReviewLimitWindow;
45
52
  readonly retryAfterMs?: number;
46
53
  } | {
@@ -70,6 +77,7 @@ export interface ReviewRunSummary {
70
77
  readonly markerMatched: boolean | null;
71
78
  readonly result?: string;
72
79
  readonly failureCode?: string;
80
+ readonly providerErrorDetail?: string;
73
81
  readonly limitWindow?: ReviewLimitWindow;
74
82
  readonly retryAfterMs?: number;
75
83
  readonly summaryPath: string;
@@ -0,0 +1,24 @@
1
+ import { type OpencodeAccountAvailabilityState } from "./availability.js";
2
+ export interface OpencodeAccount {
3
+ readonly id: string;
4
+ readonly secretName: string;
5
+ readonly key: string;
6
+ }
7
+ export interface OpencodeAccountSelection {
8
+ readonly account: OpencodeAccount;
9
+ readonly cooled: boolean;
10
+ readonly waitMs?: number;
11
+ }
12
+ /**
13
+ * Discover opencode-go accounts from Doppler-sourced secrets shaped
14
+ * `OPENCODE_GO_KEY_<ID>`. Sorted by id ascending, de-duplicated by id.
15
+ */
16
+ export declare function resolveOpencodeAccounts(secrets: Readonly<Record<string, string>>): readonly OpencodeAccount[];
17
+ export declare function opencodeAccountCanonicalId(id: string): string;
18
+ export declare function opencodeAccountDataDir(id: string, stateRoot?: string): string;
19
+ /**
20
+ * Select the account to use for the next opencode-go invocation. Callers must
21
+ * prune expired suppressions in `state` before calling this (via
22
+ * `pruneExpiredOpencodeAccountSuppressions`).
23
+ */
24
+ export declare function selectActiveOpencodeAccount(accounts: readonly OpencodeAccount[], state: OpencodeAccountAvailabilityState, nowMs?: number): OpencodeAccountSelection | undefined;
@@ -0,0 +1,68 @@
1
+ import path from "node:path";
2
+ import { getStateRoot } from "#paths/state-root.js";
3
+ import { isOpencodeAccountSuppressed, } from "./availability.js";
4
+ const OPENCODE_GO_KEY_PATTERN = /^OPENCODE_GO_KEY_([A-Za-z0-9]+)$/;
5
+ /**
6
+ * Discover opencode-go accounts from Doppler-sourced secrets shaped
7
+ * `OPENCODE_GO_KEY_<ID>`. Sorted by id ascending, de-duplicated by id.
8
+ */
9
+ export function resolveOpencodeAccounts(secrets) {
10
+ const byId = new Map();
11
+ for (const [secretName, key] of Object.entries(secrets)) {
12
+ if (!key || key.trim().length === 0)
13
+ continue;
14
+ const match = OPENCODE_GO_KEY_PATTERN.exec(secretName);
15
+ if (!match?.[1])
16
+ continue;
17
+ const id = match[1].toLowerCase();
18
+ if (!byId.has(id))
19
+ byId.set(id, { id, secretName, key });
20
+ }
21
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
22
+ }
23
+ export function opencodeAccountCanonicalId(id) {
24
+ return `opencode-go:${id}`;
25
+ }
26
+ export function opencodeAccountDataDir(id, stateRoot) {
27
+ return path.join(stateRoot ?? getStateRoot(), "opencode-accounts", id);
28
+ }
29
+ function suppressionExpiryMs(state, canonicalId) {
30
+ const record = state.accounts[canonicalId];
31
+ if (record === undefined || record.lastFailureAt === undefined)
32
+ return undefined;
33
+ const failureAt = Date.parse(record.lastFailureAt);
34
+ if (!Number.isFinite(failureAt))
35
+ return undefined;
36
+ return failureAt + (record.retryAfterMs ?? 0);
37
+ }
38
+ function pickSoonestExpiring(accounts, state, nowMs) {
39
+ const withExpiry = accounts.map((account) => ({
40
+ account,
41
+ expiresAt: suppressionExpiryMs(state, opencodeAccountCanonicalId(account.id)) ??
42
+ Number.POSITIVE_INFINITY,
43
+ }));
44
+ // `accounts` is non-empty whenever this is called, so a no-initial-value
45
+ // reduce is safe and keeps the accumulator type undefined-free.
46
+ const soonest = withExpiry.reduce((min, candidate) => candidate.expiresAt < min.expiresAt ? candidate : min);
47
+ const waitMs = Number.isFinite(soonest.expiresAt)
48
+ ? Math.max(0, soonest.expiresAt - nowMs)
49
+ : undefined;
50
+ return {
51
+ account: soonest.account,
52
+ cooled: true,
53
+ ...(waitMs === undefined ? {} : { waitMs }),
54
+ };
55
+ }
56
+ /**
57
+ * Select the account to use for the next opencode-go invocation. Callers must
58
+ * prune expired suppressions in `state` before calling this (via
59
+ * `pruneExpiredOpencodeAccountSuppressions`).
60
+ */
61
+ export function selectActiveOpencodeAccount(accounts, state, nowMs = Date.now()) {
62
+ if (accounts.length === 0)
63
+ return undefined;
64
+ const available = accounts.find((account) => !isOpencodeAccountSuppressed(state, opencodeAccountCanonicalId(account.id)));
65
+ if (available)
66
+ return { account: available, cooled: false };
67
+ return pickSoonestExpiring(accounts, state, nowMs);
68
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Resolve opencode's `auth.json` path from an XDG-style data-home env var.
3
+ *
4
+ * Mirrors the resolver in `src/cli/commands/dash/opencode-db.ts`
5
+ * (`opencodeDatabasePath`), but targets `opencode/auth.json` instead of
6
+ * `opencode/opencode.db`.
7
+ */
8
+ export declare function opencodeAuthPath(env?: Readonly<Record<string, string | undefined>>, home?: string): string;
9
+ export interface MaterializeOpencodeGoAuthOptions {
10
+ readonly dataDir?: string;
11
+ readonly env?: Readonly<Record<string, string | undefined>>;
12
+ readonly home?: string;
13
+ }
14
+ export interface MaterializeOpencodeGoAuthResult {
15
+ readonly path: string;
16
+ readonly wrote: boolean;
17
+ }
18
+ /**
19
+ * Read the `opencode-go` API key currently on disk, or `undefined` when the
20
+ * store is missing, corrupt, or holds no `opencode-go` entry.
21
+ *
22
+ * Callers use this to identify which account is currently materialized by
23
+ * matching the value against their own resolved account list — that keeps the
24
+ * "which account is active" question answerable without persisting extra state.
25
+ * Never throws.
26
+ */
27
+ export declare function readOpencodeGoKey(options?: MaterializeOpencodeGoAuthOptions): string | undefined;
28
+ /**
29
+ * Merge an `opencode-go` API-key entry into opencode's `auth.json`, preserving
30
+ * every other provider entry already on disk. Never throws on a missing,
31
+ * unreadable, or corrupt existing file — that state is treated as `{}`.
32
+ */
33
+ export declare function materializeOpencodeGoAuth(key: string, options?: MaterializeOpencodeGoAuthOptions): MaterializeOpencodeGoAuthResult;
@@ -0,0 +1,79 @@
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
5
+ /**
6
+ * Resolve opencode's `auth.json` path from an XDG-style data-home env var.
7
+ *
8
+ * Mirrors the resolver in `src/cli/commands/dash/opencode-db.ts`
9
+ * (`opencodeDatabasePath`), but targets `opencode/auth.json` instead of
10
+ * `opencode/opencode.db`.
11
+ */
12
+ export function opencodeAuthPath(env = process.env, home = homedir()) {
13
+ const dataHome = env["XDG_DATA_HOME"]?.trim() || join(home, ".local", "share");
14
+ return join(dataHome, "opencode", "auth.json");
15
+ }
16
+ function resolveAuthPath(options) {
17
+ if (options.dataDir !== undefined) {
18
+ return join(options.dataDir, "opencode", "auth.json");
19
+ }
20
+ return opencodeAuthPath(options.env, options.home);
21
+ }
22
+ function readExistingAuth(path) {
23
+ let raw;
24
+ try {
25
+ raw = readFileSync(path, "utf8");
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
33
+ return parsed;
34
+ }
35
+ return {};
36
+ }
37
+ catch {
38
+ return {};
39
+ }
40
+ }
41
+ function isSameOpencodeGoEntry(entry, key) {
42
+ if (entry === null || typeof entry !== "object")
43
+ return false;
44
+ const candidate = entry;
45
+ return candidate["type"] === "api" && candidate["key"] === key;
46
+ }
47
+ /**
48
+ * Read the `opencode-go` API key currently on disk, or `undefined` when the
49
+ * store is missing, corrupt, or holds no `opencode-go` entry.
50
+ *
51
+ * Callers use this to identify which account is currently materialized by
52
+ * matching the value against their own resolved account list — that keeps the
53
+ * "which account is active" question answerable without persisting extra state.
54
+ * Never throws.
55
+ */
56
+ export function readOpencodeGoKey(options = {}) {
57
+ const entry = readExistingAuth(resolveAuthPath(options))["opencode-go"];
58
+ if (entry === null || typeof entry !== "object")
59
+ return undefined;
60
+ const candidate = entry;
61
+ return typeof candidate["key"] === "string" ? candidate["key"] : undefined;
62
+ }
63
+ /**
64
+ * Merge an `opencode-go` API-key entry into opencode's `auth.json`, preserving
65
+ * every other provider entry already on disk. Never throws on a missing,
66
+ * unreadable, or corrupt existing file — that state is treated as `{}`.
67
+ */
68
+ export function materializeOpencodeGoAuth(key, options = {}) {
69
+ const path = resolveAuthPath(options);
70
+ const existing = readExistingAuth(path);
71
+ if (isSameOpencodeGoEntry(existing["opencode-go"], key)) {
72
+ return { path, wrote: false };
73
+ }
74
+ const entry = { type: "api", key };
75
+ const next = { ...existing, "opencode-go": entry };
76
+ mkdirSync(dirname(path), { recursive: true });
77
+ writeJsonFileAtomic(path, next, { writeFileOptions: { mode: 0o600 } });
78
+ return { path, wrote: true };
79
+ }
@@ -12,7 +12,19 @@ interface TreeEntry {
12
12
  readonly path: Buffer;
13
13
  }
14
14
  export declare function resolveReviewCommit(cwd: string, ref: string): string;
15
+ /**
16
+ * Repo-relative paths with uncommitted changes (staged, unstaged, or
17
+ * untracked) that are NOT review evidence (reviews.md / review-events.jsonl /
18
+ * review-artifacts/*.md — see isReviewEvidenceRelativePath). A `wp-git-tree-v1`
19
+ * subject digest is computed purely from the committed tree at a ref, so any
20
+ * non-evidence path here means the working tree diverges from what that
21
+ * digest — and any approval bound to it — actually covers. Review evidence is
22
+ * excluded because the gate's own artifact/ledger writes routinely leave the
23
+ * tree dirty between reruns without representing unreviewed content.
24
+ */
25
+ export declare function findUnreviewedWorkingTreeChanges(cwd: string): readonly string[];
15
26
  declare function isExcludedReviewEnvelope(entry: TreeEntry, blueprintRoot: Buffer): boolean;
27
+ export declare function isReviewEvidenceRelativePath(value: string): boolean;
16
28
  declare function isReviewEvidencePath(entry: TreeEntry): boolean;
17
29
  declare function updateField(hash: Hash, value: Buffer | string): void;
18
30
  export declare function createDeliverySubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
@@ -39,6 +39,43 @@ export function resolveReviewCommit(cwd, ref) {
39
39
  }
40
40
  return commit;
41
41
  }
42
+ /**
43
+ * Repo-relative paths with uncommitted changes (staged, unstaged, or
44
+ * untracked) that are NOT review evidence (reviews.md / review-events.jsonl /
45
+ * review-artifacts/*.md — see isReviewEvidenceRelativePath). A `wp-git-tree-v1`
46
+ * subject digest is computed purely from the committed tree at a ref, so any
47
+ * non-evidence path here means the working tree diverges from what that
48
+ * digest — and any approval bound to it — actually covers. Review evidence is
49
+ * excluded because the gate's own artifact/ledger writes routinely leave the
50
+ * tree dirty between reruns without representing unreviewed content.
51
+ */
52
+ export function findUnreviewedWorkingTreeChanges(cwd) {
53
+ const output = gitBuffer(cwd, [
54
+ "status",
55
+ "--porcelain=v1",
56
+ "--untracked-files=all",
57
+ "--ignored=no",
58
+ "-z",
59
+ ]).toString("utf8");
60
+ const records = output.split("\0").filter((record) => record.length > 0);
61
+ const paths = new Set();
62
+ for (let index = 0; index < records.length; index += 1) {
63
+ const record = records[index];
64
+ const statusCode = record.slice(0, 2);
65
+ paths.add(record.slice(3));
66
+ if (/[RC]/u.test(statusCode)) {
67
+ // Rename/copy porcelain records are followed by the original path.
68
+ const originalPath = records[index + 1];
69
+ if (originalPath !== undefined) {
70
+ paths.add(originalPath);
71
+ index += 1;
72
+ }
73
+ }
74
+ }
75
+ return [...paths]
76
+ .filter((relativePath) => !isReviewEvidenceRelativePath(relativePath))
77
+ .toSorted();
78
+ }
42
79
  function parseTree(cwd, commit) {
43
80
  const output = gitBuffer(cwd, ["ls-tree", "-r", "-z", "--full-tree", commit]);
44
81
  const entries = [];
@@ -74,31 +111,40 @@ function parseTree(cwd, commit) {
74
111
  throw new Error("Truncated git ls-tree output.");
75
112
  return entries.sort((left, right) => Buffer.compare(left.path, right.path));
76
113
  }
77
- function findBlueprintRoot(entries, blueprintSlug, blueprintPath) {
114
+ function isRegularBlob(entry) {
115
+ return entry.type === "blob" && /^100(?:644|755)$/u.test(entry.mode.toString("ascii"));
116
+ }
117
+ function findBlueprintDocument(entries, blueprintSlug, blueprintPath) {
78
118
  if (blueprintPath) {
79
119
  const normalized = blueprintPath.replaceAll("\\", "/");
80
120
  const target = Buffer.from(normalized, "utf8");
81
- const matches = entries.filter((entry) => entry.path.equals(target));
82
- if (matches.length === 1 && normalized.endsWith("/_overview.md")) {
83
- return target.subarray(0, target.length - Buffer.byteLength("_overview.md"));
121
+ const matches = entries.filter((entry) => entry.path.equals(target) && isRegularBlob(entry));
122
+ if (matches.length === 1 && normalized.endsWith(".md")) {
123
+ return matches[0];
84
124
  }
85
125
  }
86
- const suffix = Buffer.from(`/${blueprintSlug}/_overview.md`, "utf8");
126
+ const folderSuffix = `/${blueprintSlug}/_overview.md`;
127
+ const flatSuffix = `/${blueprintSlug}.md`;
87
128
  const matches = entries.filter((entry) => {
88
129
  const normalized = entry.path.toString("utf8");
89
- const segments = normalized.split("/");
90
- return (segments[0] === "blueprints" &&
91
- BLUEPRINT_STATUSES.has(segments[1] ?? "") &&
92
- entry.path.subarray(-suffix.length).equals(suffix));
130
+ const suffix = normalized.endsWith(folderSuffix)
131
+ ? folderSuffix
132
+ : normalized.endsWith(flatSuffix)
133
+ ? flatSuffix
134
+ : undefined;
135
+ const lifecycle = suffix ? normalized.slice(0, -suffix.length).split("/").at(-1) : undefined;
136
+ return isRegularBlob(entry) && BLUEPRINT_STATUSES.has(lifecycle ?? "");
93
137
  });
94
138
  if (matches.length !== 1) {
95
139
  throw new Error(`Expected one tracked blueprint ${blueprintSlug} at target commit, found ${matches.length}.`);
96
140
  }
97
- const overview = matches[0].path;
98
- return overview.subarray(0, overview.length - Buffer.byteLength("_overview.md"));
141
+ return matches[0];
99
142
  }
100
- function isRegularBlob(entry) {
101
- return entry.type === "blob" && /^100(?:644|755)$/u.test(entry.mode.toString("ascii"));
143
+ function blueprintEvidenceRoot(documentPath) {
144
+ const value = documentPath.toString("utf8");
145
+ if (!value.endsWith("/_overview.md"))
146
+ return null;
147
+ return documentPath.subarray(0, documentPath.length - Buffer.byteLength("_overview.md"));
102
148
  }
103
149
  function isExcludedReviewEnvelope(entry, blueprintRoot) {
104
150
  if (!isRegularBlob(entry) ||
@@ -114,10 +160,10 @@ function isExcludedReviewEnvelope(entry, blueprintRoot) {
114
160
  const artifactName = relative.slice(prefix.length);
115
161
  return artifactName.length > 3 && !artifactName.includes("/") && artifactName.endsWith(".md");
116
162
  }
117
- function isReviewEvidencePath(entry) {
118
- if (!isRegularBlob(entry))
119
- return false;
120
- const value = entry.path.toString("utf8");
163
+ // String-based counterpart of isReviewEvidencePath, shared with callers that
164
+ // only have a repo-relative path (no TreeEntry) — currently the working-tree
165
+ // divergence check in findUnreviewedWorkingTreeChanges below.
166
+ export function isReviewEvidenceRelativePath(value) {
121
167
  if (!/^blueprints\/(?:[^/]+\/)*[^/]+\//u.test(value))
122
168
  return false;
123
169
  if (/(?:^|\/)reviews\.md$/u.test(value) || /(?:^|\/)review-events\.jsonl$/u.test(value)) {
@@ -125,6 +171,11 @@ function isReviewEvidencePath(entry) {
125
171
  }
126
172
  return /(?:^|\/)review-artifacts\/[^/]+\.md$/u.test(value);
127
173
  }
174
+ function isReviewEvidencePath(entry) {
175
+ if (!isRegularBlob(entry))
176
+ return false;
177
+ return isReviewEvidenceRelativePath(entry.path.toString("utf8"));
178
+ }
128
179
  function loadBlobContents(cwd, entries) {
129
180
  const blobs = entries.filter((entry) => entry.type === "blob");
130
181
  if (blobs.length === 0)
@@ -195,11 +246,13 @@ function readTreeAtRef(cwd, ref) {
195
246
  }
196
247
  export function createDeliverySubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
197
248
  const { commit, objectFormat, entries: allEntries } = readTreeAtRef(cwd, ref);
198
- const blueprintRoot = findBlueprintRoot(allEntries, blueprintSlug, blueprintPath);
249
+ const document = findBlueprintDocument(allEntries, blueprintSlug, blueprintPath);
250
+ const blueprintRoot = blueprintEvidenceRoot(document.path);
199
251
  // Exclude review evidence for the owning blueprint AND for every other blueprint
200
252
  // (the same global exclusion source snapshots use): recording evidence under an
201
253
  // unrelated blueprint must not invalidate an otherwise-identical delivery review.
202
- const entries = allEntries.filter((entry) => !isExcludedReviewEnvelope(entry, blueprintRoot) && !isReviewEvidencePath(entry));
254
+ const entries = allEntries.filter((entry) => (blueprintRoot === null || !isExcludedReviewEnvelope(entry, blueprintRoot)) &&
255
+ !isReviewEvidencePath(entry));
203
256
  return {
204
257
  scheme: "wp-git-tree-v1",
205
258
  digest: hashTreeEntries(cwd, entries, objectFormat, DELIVERY_DOMAIN),
@@ -413,9 +466,8 @@ export function legacyNormalizePlanSubjectContent(content) {
413
466
  function planSubjectAtRef(cwd, ref, blueprintSlug, normalize, blueprintPath) {
414
467
  const commit = resolveReviewCommit(cwd, ref);
415
468
  const entries = parseTree(cwd, commit);
416
- const blueprintRoot = findBlueprintRoot(entries, blueprintSlug, blueprintPath);
417
- const overviewPath = Buffer.concat([blueprintRoot, Buffer.from("_overview.md", "ascii")]);
418
- const overview = entries.find((entry) => entry.path.equals(overviewPath));
469
+ const overview = findBlueprintDocument(entries, blueprintSlug, blueprintPath);
470
+ const overviewPath = overview.path;
419
471
  if (!overview || overview.type !== "blob") {
420
472
  throw new Error("Tracked blueprint overview is not a blob.");
421
473
  }
@@ -0,0 +1,34 @@
1
+ import { type OpencodeAccount } from "#review/opencode-account.js";
2
+ import { resolveRuntimeEnvironment } from "./executor.js";
3
+ export type OpencodeAccountsResolution = {
4
+ readonly ok: true;
5
+ readonly accounts: readonly OpencodeAccount[];
6
+ } | {
7
+ readonly ok: false;
8
+ readonly reason: string;
9
+ };
10
+ export interface ResolveOpencodeAccountsGracefullyInput {
11
+ readonly cwd: string;
12
+ /**
13
+ * Secret profile to resolve through — REQUIRED, never optional.
14
+ *
15
+ * Without it `resolveRuntimeEnvironment` passes no environment selector and
16
+ * the secret manager falls back to its own ambient, path-keyed config
17
+ * (Doppler's per-directory scopes, Infisical's discovered `.infisical.json`).
18
+ * That resolves only in a checkout that happens to sit under the scoped path,
19
+ * so discovery silently yields zero accounts in every managed worktree, CI
20
+ * clone, and fresh checkout — a failure mode that is invisible because callers
21
+ * degrade gracefully. Keeping this required makes that class of bug a compile
22
+ * error instead of a silent runtime degradation. Callers pass
23
+ * `resolveAgentSecretProfile(env)`.
24
+ */
25
+ readonly secretProfile: string;
26
+ readonly resolveRuntimeEnvironment?: typeof resolveRuntimeEnvironment;
27
+ }
28
+ /**
29
+ * Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
30
+ * On any resolution failure (missing profile, secret-manager error, empty
31
+ * result), returns a failure reason derived only from the error's own
32
+ * message/type — never from a secret value.
33
+ */
34
+ export declare function resolveOpencodeAccountsGracefully(input: ResolveOpencodeAccountsGracefullyInput): OpencodeAccountsResolution;
@@ -0,0 +1,33 @@
1
+ import { resolveOpencodeAccounts } from "#review/opencode-account.js";
2
+ import { resolveRuntimeEnvironment } from "./executor.js";
3
+ function describeError(error) {
4
+ if (error instanceof Error)
5
+ return `${error.name}: ${error.message}`;
6
+ return typeof error === "string" ? error : "unknown error";
7
+ }
8
+ /**
9
+ * Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
10
+ * On any resolution failure (missing profile, secret-manager error, empty
11
+ * result), returns a failure reason derived only from the error's own
12
+ * message/type — never from a secret value.
13
+ */
14
+ export function resolveOpencodeAccountsGracefully(input) {
15
+ const resolveEnv = input.resolveRuntimeEnvironment ?? resolveRuntimeEnvironment;
16
+ let secrets;
17
+ try {
18
+ secrets = resolveEnv({
19
+ cwd: input.cwd,
20
+ profile: "secrets-only",
21
+ secretProfile: input.secretProfile,
22
+ environment: input.secretProfile,
23
+ });
24
+ }
25
+ catch (error) {
26
+ return { ok: false, reason: describeError(error) };
27
+ }
28
+ const accounts = resolveOpencodeAccounts(secrets);
29
+ if (accounts.length === 0) {
30
+ return { ok: false, reason: "no OPENCODE_GO_KEY_* secrets found" };
31
+ }
32
+ return { ok: true, accounts };
33
+ }